Skip to content

Commit

Permalink
Add support for activity scoped navigation request handlers
Browse files Browse the repository at this point in the history
This gives more flexibility for applications that follows a complex UI design
  • Loading branch information
deepueg committed Jun 29, 2020
1 parent 29d2322 commit 5b0e5ab
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@

import static com.ern.api.impl.core.ActivityDelegateConstants.KEY_REGISTER_NAV_VIEW_MODEL;
import static com.ern.api.impl.core.ElectrodeReactFragmentDelegate.MiniAppRequestListener.ADD_TO_BACKSTACK;
import static com.ern.api.impl.core.LaunchConfig.NONE;

public class ElectrodeBaseActivityDelegate extends ElectrodeReactActivityDelegate implements LifecycleObserver {
public class ElectrodeBaseActivityDelegate<T extends LaunchConfig> extends ElectrodeReactActivityDelegate implements LifecycleObserver {
private static final String TAG = ElectrodeBaseActivityDelegate.class.getSimpleName();

@SuppressWarnings("WeakerAccess")
protected FragmentActivity mFragmentActivity;
private final LaunchConfig mDefaultLaunchConfig;
protected final T mDefaultLaunchConfig;
private final String mRootComponentName;

/**
Expand All @@ -36,7 +37,7 @@ public class ElectrodeBaseActivityDelegate extends ElectrodeReactActivityDelegat
* @param defaultLaunchConfig : {@link LaunchConfig} that acts as the the initial configuration to load the rootComponent as well as the default launch config for subsequent navigation flows.
* This configuration will also be used as a default configuration when the root component tries to navigate to a new pages if a proper launch config is passed inside {@link #startMiniAppFragment(String, LaunchConfig)}.
*/
public ElectrodeBaseActivityDelegate(@NonNull FragmentActivity activity, @Nullable String rootComponentName, @NonNull LaunchConfig defaultLaunchConfig) {
public ElectrodeBaseActivityDelegate(@NonNull FragmentActivity activity, @Nullable String rootComponentName, @NonNull T defaultLaunchConfig) {
super(activity, null);

mFragmentActivity = activity;
Expand Down Expand Up @@ -145,13 +146,17 @@ public void startMiniAppFragment(@NonNull String componentName, @NonNull LaunchC
switchToFragment(fragment, launchConfig, componentName);
}

protected boolean fragmentScopedNavModel() {
return true;
}

private void switchToFragment(@NonNull Fragment fragment, @NonNull LaunchConfig launchConfig, @Nullable String tag) {
if (fragment instanceof DialogFragment) {
Logger.d(TAG, "Showing dialog fragment");
((DialogFragment) fragment).show(getFragmentManager(launchConfig), tag);
} else {
final FragmentManager fragmentManager = getFragmentManager(launchConfig);
int fragmentContainerId = (launchConfig.mFragmentContainerId != LaunchConfig.NONE) ? launchConfig.mFragmentContainerId : mDefaultLaunchConfig.mFragmentContainerId;
int fragmentContainerId = (launchConfig.mFragmentContainerId != NONE) ? launchConfig.mFragmentContainerId : mDefaultLaunchConfig.mFragmentContainerId;

final FragmentTransaction transaction = fragmentManager.beginTransaction();
manageTransition(transaction);
Expand All @@ -161,14 +166,14 @@ private void switchToFragment(@NonNull Fragment fragment, @NonNull LaunchConfig
transaction.addToBackStack(tag);
}

if (fragmentContainerId != LaunchConfig.NONE) {
if (fragmentContainerId != NONE) {
if (launchConfig.mShowAsOverlay) {
Logger.d(TAG, "performing ADD fragment inside fragment container");
transaction.add(fragmentContainerId, fragment, tag);
} else {
Logger.d(TAG, "performing REPLACE fragment inside fragment container");
if (fragment.getArguments() != null) {
fragment.getArguments().putBoolean(KEY_REGISTER_NAV_VIEW_MODEL, true);
fragment.getArguments().putBoolean(KEY_REGISTER_NAV_VIEW_MODEL, fragmentScopedNavModel());
}
transaction.replace(fragmentContainerId, fragment, tag);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import androidx.annotation.Nullable;

/**
* Used for updating the props for a React Native view component
* Used for updating the props for a React Native view component.
* This is mainly used while going back to a previous fragment that requires new props being passed to the previous fragment.
* Refer {@link com.ern.api.impl.navigation.MiniAppNavigationFragment} for usage
*/
public interface UpdatePropsListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

import com.ern.api.impl.core.ElectrodeBaseActivityDelegate;
import com.ern.api.impl.core.LaunchConfig;
import com.facebook.react.ReactRootView;
import com.walmartlabs.electrode.reactnative.bridge.helpers.Logger;

import org.json.JSONObject;

public abstract class ElectrodeBaseActivity extends AppCompatActivity implements ElectrodeNavigationActivityListener {
public static final int DEFAULT_TITLE = -1;
public static final int NONE = 0;

protected ElectrodeBaseActivityDelegate mElectrodeReactNavDelegate;
private static final String TAG = ElectrodeBaseActivity.class.getSimpleName();

protected ElectrodeNavigationActivityDelegate mElectrodeReactNavDelegate;

/**
* Override and provide the main layout resource.
Expand All @@ -42,7 +44,7 @@ public abstract class ElectrodeBaseActivity extends AppCompatActivity implements
*/
@NonNull
protected abstract String getRootComponentName();

/**
* Id for the fragment container.
*
Expand Down Expand Up @@ -70,7 +72,7 @@ protected Bundle getProps() {
protected int title() {
return DEFAULT_TITLE;
}

/**
* Initial/Default fragment used by the activity to host a react native view.
*
Expand All @@ -96,12 +98,39 @@ protected boolean hideNavBar() {
* @return ElectrodeReactFragmentActivityDelegate
*/
@NonNull
protected ElectrodeBaseActivityDelegate createElectrodeDelegate() {
return new ElectrodeBaseActivityDelegate(this, getRootComponentName(), createDefaultLaunchConfig());
protected ElectrodeNavigationActivityDelegate createElectrodeDelegate() {
return new ElectrodeNavigationActivityDelegate(this, getRootComponentName(), createNavigationLaunchConfig());
}

/**
* Default configuration provided for navigation.
*
* @return NavigationLaunchConfig
*/
protected NavigationLaunchConfig createNavigationLaunchConfig() {
LaunchConfig navConfig = createDefaultLaunchConfig();
if (navConfig instanceof NavigationLaunchConfig) {
return (NavigationLaunchConfig) navConfig;
} else {
if (navConfig != null) {
Logger.w(TAG, "createDefaultLaunchConfig() is Deprecated, your default launch config is IGNORED, please move to createNavigationLaunchConfig");
}
return createNavLaunchConfigInternal();
}
}

/**
* Keeping this method for backward compatibility
*
* @deprecated use {@link #createNavigationLaunchConfig()}
*/
@Deprecated
protected LaunchConfig createDefaultLaunchConfig() {
LaunchConfig defaultLaunchConfig = new LaunchConfig();
return createNavLaunchConfigInternal();
}

private NavigationLaunchConfig createNavLaunchConfigInternal() {
NavigationLaunchConfig defaultLaunchConfig = new NavigationLaunchConfig();
defaultLaunchConfig.setFragmentClass(miniAppFragmentClass());
defaultLaunchConfig.setFragmentContainerId(getFragmentContainerId());
defaultLaunchConfig.setFragmentManager(getSupportFragmentManager());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.ern.api.impl.navigation;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Observer;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.lifecycle.ViewModelProvider;

import com.ern.api.impl.core.ElectrodeBaseActivityDelegate;
import com.ern.api.impl.core.LaunchConfig;
import com.walmartlabs.electrode.reactnative.bridge.helpers.Logger;

public class ElectrodeNavigationActivityDelegate extends ElectrodeBaseActivityDelegate<NavigationLaunchConfig> {

private static final String TAG = ElectrodeNavigationActivityDelegate.class.getSimpleName();

@Nullable
private ReactNavigationViewModel mNavViewModel;

/**
* @param activity Hosting activity
* @param rootComponentName First react native component to be launched.
* @param defaultLaunchConfig : {@link LaunchConfig} that acts as the the initial configuration to load the rootComponent as well as the default launch config for subsequent navigation flows.
* This configuration will also be used as a default configuration when the root component tries to navigate to a new pages if a proper launch config is passed inside {@link #startMiniAppFragment(String, LaunchConfig)}.
*/
public ElectrodeNavigationActivityDelegate(@NonNull FragmentActivity activity, @Nullable String rootComponentName, @NonNull NavigationLaunchConfig defaultLaunchConfig) {
super(activity, rootComponentName, defaultLaunchConfig);
}

@Override
public void onCreate(Bundle savedInstanceState) {
if (mDefaultLaunchConfig.mUseActivityScopedNavigation) {
registerNavRequestHandler();
}
super.onCreate(savedInstanceState);
}

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
@Override
public void onResume() {
super.onResume();
if (mDefaultLaunchConfig.mUseActivityScopedNavigation && mNavViewModel != null) {
mNavViewModel.registerNavRequestHandler();
}
}

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
@Override
public void onPause() {
super.onPause();
if (mDefaultLaunchConfig.mUseActivityScopedNavigation && mNavViewModel != null) {
mNavViewModel.unRegisterNavRequestHandler();
}
}

private void registerNavRequestHandler() {
mNavViewModel = new ViewModelProvider(mFragmentActivity, new ViewModelProvider.NewInstanceFactory()).get(ReactNavigationViewModel.class);
mNavViewModel.getRouteLiveData().observe(mFragmentActivity, new Observer<Route>() {
@Override
public void onChanged(Route route) {
Logger.d(TAG, "Navigation request handled by Activity(%s)", mFragmentActivity);
NavigationRouteHandler routeHandler = null;
if (mDefaultLaunchConfig.mRouteHandlerProvider != null && mDefaultLaunchConfig.mRouteHandlerProvider.getRouteHandler() != null) {
Logger.v(TAG, "Getting route handler via RouteHandlerProvider");
routeHandler = mDefaultLaunchConfig.mRouteHandlerProvider.getRouteHandler();
} else {
Fragment f = mFragmentActivity.getSupportFragmentManager().findFragmentById(mDefaultLaunchConfig.getFragmentContainerId());
if (f instanceof NavigationRouteHandler) {
Logger.v(TAG, "Getting fragment(route handler) hosted inside getFragmentContainer of activity");
routeHandler = (NavigationRouteHandler) f;
} else {
Logger.w(TAG, "Fragment: %s is not a NavigationRouteHandler", f);
}
}

if (routeHandler != null) {
routeHandler.handleRoute(route);
} else {
throw new IllegalStateException("Should never reach here. Missing route handler");
}
}
});
mNavViewModel.registerNavRequestHandler();
}

protected boolean fragmentScopedNavModel() {
return !mDefaultLaunchConfig.mUseActivityScopedNavigation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,64 +57,68 @@ public class ElectrodeNavigationFragmentDelegate<T extends ElectrodeBaseFragment
private final Observer<Route> routeObserver = new Observer<Route>() {
@Override
public void onChanged(@Nullable Route route) {
if (route != null && !route.isCompleted()) {
Logger.d(TAG, "Delegate: %s received a new navigation route: %s", ElectrodeNavigationFragmentDelegate.this, route.getArguments());
handleRoute(route, null);
}
};

if (!route.getArguments().containsKey(KEY_NAV_TYPE)) {
throw new IllegalStateException("Missing NAV_TYPE in route arguments");
}
Fragment topOfTheStackFragment = getTopOfTheStackFragment();

//NOTE: We can't put KEY_NAV_TYPE as a parcelable since ReactNative side does not support Parcelable deserialization yet.
ReactNavigationViewModel.Type navType = ReactNavigationViewModel.Type.valueOf(route.getArguments().getString(KEY_NAV_TYPE));
if (topOfTheStackFragment == mFragment) {
switch (navType) {
case NAVIGATE:
navigate(route);
break;
case UPDATE:
update(route);
break;
case BACK:
back(route);
break;
case FINISH:
finish(route);
break;
}
} else if (topOfTheStackFragment instanceof ComponentAsOverlay) {
// When the top of the stack fragment is an overlay, the non-overlay fragment below it acts as the parent who is handling it's overlay children and it's navigation flows.
// This is because, overlay fragments are added to the stack by calling FragmentTransactionManager's add() method and not replace().
// When added, the new fragment's onResume() state is reached by keeping the parent fragment also in a resumed state. Hence the parent need to delegate the navigation calls to the child overlays.
Logger.i(TAG, "Delegating %s request to an overlay component.", navType);
ComponentAsOverlay overlayFragment = (ComponentAsOverlay) topOfTheStackFragment;
switch (navType) {
case NAVIGATE:
overlayFragment.navigate(route);
break;
case UPDATE:
overlayFragment.update(route);
break;
case BACK:
overlayFragment.back(route);
break;
case FINISH:
overlayFragment.finish(route);
break;
}
} else {
throw new RuntimeException("Should never reach here. The fragment handling a navigation api request should be either the current fragment or the top of the stack fragment should implement ComponentAsOverlay. topOfTheStackFragment:" + topOfTheStackFragment);
}
protected void handleRoute(@Nullable Route route, @Nullable Fragment currentVisibleFragment) {
if (route != null && !route.isCompleted()) {
Logger.d(TAG, "Delegate: %s received a new navigation route: %s", ElectrodeNavigationFragmentDelegate.this, route.getArguments());

if (!route.isCompleted()) {
throw new RuntimeException("Should never reach here. A result should be set for the route at this point. Make sure a setResult is called on the route object after the appropriate action is taken on a navigation request");
if (!route.getArguments().containsKey(KEY_NAV_TYPE)) {
throw new IllegalStateException("Missing NAV_TYPE in route arguments");
}
Fragment topOfTheStackFragment = currentVisibleFragment != null ? currentVisibleFragment : getTopOfTheStackFragment();

//NOTE: We can't put KEY_NAV_TYPE as a parcelable since ReactNative side does not support Parcelable deserialization yet.
ReactNavigationViewModel.Type navType = ReactNavigationViewModel.Type.valueOf(route.getArguments().getString(KEY_NAV_TYPE));
if (topOfTheStackFragment == mFragment) {
switch (navType) {
case NAVIGATE:
navigate(route);
break;
case UPDATE:
update(route);
break;
case BACK:
back(route);
break;
case FINISH:
finish(route);
break;
}
} else if (topOfTheStackFragment instanceof ComponentAsOverlay) {
// When the top of the stack fragment is an overlay, the non-overlay fragment below it acts as the parent who is handling it's overlay children and it's navigation flows.
// This is because, overlay fragments are added to the stack by calling FragmentTransactionManager's add() method and not replace().
// When added, the new fragment's onResume() state is reached by keeping the parent fragment also in a resumed state. Hence the parent need to delegate the navigation calls to the child overlays.
Logger.i(TAG, "Delegating %s request to an overlay component.", navType);
ComponentAsOverlay overlayFragment = (ComponentAsOverlay) topOfTheStackFragment;
switch (navType) {
case NAVIGATE:
overlayFragment.navigate(route);
break;
case UPDATE:
overlayFragment.update(route);
break;
case BACK:
overlayFragment.back(route);
break;
case FINISH:
overlayFragment.finish(route);
break;
}
Logger.d(TAG, "Nav request handling completed by: %s", topOfTheStackFragment);
} else {
Logger.d(TAG, "Delegate: %s has ignored an already handled route: %s, ", ElectrodeNavigationFragmentDelegate.this, route != null ? route.getArguments() : null);
throw new RuntimeException("Should never reach here. The fragment handling a navigation api request should be either the current fragment or the top of the stack fragment should implement ComponentAsOverlay. topOfTheStackFragment:" + topOfTheStackFragment);
}

if (!route.isCompleted()) {
throw new RuntimeException("Should never reach here. A result should be set for the route at this point. Make sure a setResult is called on the route object after the appropriate action is taken on a navigation request");
}
Logger.d(TAG, "Nav request handling completed by: %s", topOfTheStackFragment);
} else {
Logger.d(TAG, "Delegate: %s has ignored an already handled route: %s, ", ElectrodeNavigationFragmentDelegate.this, route != null ? route.getArguments() : null);
}
};
}

/**
* @param fragment {@link Fragment} current Fragment
Expand Down
Loading

0 comments on commit 5b0e5ab

Please sign in to comment.