package architect; import android.content.Context; import android.view.View; import java.util.ArrayList; import java.util.List; import mortar.MortarScope; import mortar.Scoped; /** * @author Lukasz Piliszczuk - lukasz.pili@gmail.com */ public class Navigator implements Scoped { public static final String SCOPE_NAME = Navigator.class.getName(); public static final String SERVICE_NAME = Navigator.class.getName(); public static Navigator get(Context context) { //noinspection ResourceType return (Navigator) context.getSystemService(SERVICE_NAME); } public static Navigator get(View view) { return get(view.getContext()); } /** * Retreive the navigator from the nearest child of the current context * Use this method from the host of a navigator container view to retrieve the associated navigator */ public static Navigator find(Context context) { MortarScope scope = MortarScope.findChild(context, SCOPE_NAME); return scope != null ? scope.<Navigator>getService(SERVICE_NAME) : null; } public static Navigator create(MortarScope containerScope, StackableParceler parceler) { return create(containerScope, parceler, null); } public static Navigator create(MortarScope containerScope, StackableParceler parceler, Config config) { if (config == null) { config = new Config(); } Preconditions.checkNotNull(containerScope, "Mortar scope for Navigator cannot be null"); Preconditions.checkArgument(config.dontRestoreStackAfterKill || parceler != null, "StackableParceler for Navigator cannot be null"); Navigator navigator = new Navigator(parceler, config); MortarScope scope = containerScope.buildChild() .withService(SERVICE_NAME, navigator) .build(SCOPE_NAME); scope.register(navigator); return navigator; } final Config config; final History history; final Transitions transitions; final Presenter presenter; final NavigatorLifecycleDelegate delegate; final Dispatcher dispatcher; private MortarScope scope; private Navigator(StackableParceler parceler, Config config) { this.config = config; history = new History(parceler); transitions = new Transitions(); delegate = new NavigatorLifecycleDelegate(this); dispatcher = new Dispatcher(this); presenter = new Presenter(transitions); } /** * Push one path */ public void push(StackablePath path) { dispatcher.dispatch(add(History.NAV_TYPE_PUSH, path)); } /** * Push one or several paths */ public void push(StackablePath... paths) { dispatcher.dispatch(add(History.NAV_TYPE_PUSH, paths)); } /** * Push navigation stack on top of the current one */ public void push(NavigationStack stack) { dispatcher.dispatch(add(History.NAV_TYPE_PUSH, stack)); } /** * Show one path as modal */ public void show(StackablePath path) { dispatcher.dispatch(add(History.NAV_TYPE_MODAL, path)); } /** * Show several paths as modal */ public void show(StackablePath... paths) { dispatcher.dispatch(add(History.NAV_TYPE_MODAL, paths)); } /** * Show navigation stack on top of current one */ public void show(NavigationStack stack) { dispatcher.dispatch(add(History.NAV_TYPE_MODAL, stack)); } /** * Replace current path with new one */ public void replace(StackablePath path) { check(); history.kill(); dispatcher.dispatch(add(History.NAV_TYPE_PUSH, path)); } /** * Replace current path with several new ones */ public void replace(StackablePath... paths) { check(); history.kill(); dispatcher.dispatch(add(History.NAV_TYPE_PUSH, paths)); } /** * Replace current path with new stack */ public void replace(NavigationStack stack) { check(); history.kill(); dispatcher.dispatch(add(History.NAV_TYPE_PUSH, stack)); } /** * Execute several navigation event on the current stack */ public void chain(NavigationChain chain) { chain(chain, null); } /** * Execute several navigation event on the current stack */ public void chain(NavigationChain chain, ViewTransitionDirection direction) { check(); Preconditions.checkArgument(chain != null && !chain.chains.isEmpty(), "Navigation chain cannot be null nor empty"); ViewTransitionDirection defaultDirection = null; List<History.Entry> entries = new ArrayList<>(chain.chains.size()); for (int i = 0; i < chain.chains.size(); i++) { NavigationChain.Chain c = chain.chains.get(i); if (c.path == null) { defaultDirection = ViewTransitionDirection.BACKWARD; if (history.canKill()) { if (c.type == NavigationChain.Chain.TYPE_BACK) { entries.add(history.kill()); } else { entries.addAll(history.killAll(false)); } } } else { defaultDirection = ViewTransitionDirection.FORWARD; if (c.type == NavigationChain.Chain.TYPE_REPLACE) { History.Entry e = history.kill(); if (history.indexOf(e) != 0) { entries.add(e); } } // push type for push and replace, modal for show int type = c.type == NavigationChain.Chain.TYPE_PUSH || c.type == NavigationChain.Chain.TYPE_REPLACE ? History.NAV_TYPE_PUSH : History.NAV_TYPE_MODAL; entries.add(history.add(c.path, type)); } } entries.get(entries.size() - 1).direction = direction != null ? direction : defaultDirection; entries.get(0).returnsResult = chain.result; dispatcher.dispatch(entries); } /** * Set new navigation stack by replacing the current one * * @param direction specify the ViewTransition direction to apply */ public void set(NavigationStack stack, ViewTransitionDirection direction) { check(); List<History.Entry> entries = new ArrayList<>(); entries.addAll(history.killAll(true)); entries.addAll(add(History.NAV_TYPE_PUSH, stack)); entries.get(entries.size() - 1).direction = direction; dispatcher.dispatch(entries); } public boolean back() { return back(null); } public boolean back(Object withResult) { check(); if (!history.canKill()) { return false; } History.Entry entry = history.kill(); if (withResult != null) { entry.returnsResult = withResult; } dispatcher.dispatch(entry); return true; } public boolean backToRoot() { return backToRoot(null); } public boolean backToRoot(Object result) { check(); if (!history.canKill()) { return false; } List<History.Entry> entries = history.killAll(false); if (result != null) { entries.get(0).returnsResult = result; } // entries.get(entries.size() - 1).transitionDirection = TransitionDirection.BACKWARD; dispatcher.dispatch(entries); return true; } private List<History.Entry> add(int navType, NavigationStack stack) { Preconditions.checkArgument(stack != null && !stack.paths.isEmpty(), "Navigation stack cannot be null nor empty"); return add(navType, stack.paths.toArray(new StackablePath[stack.paths.size()])); } private List<History.Entry> add(int navType, StackablePath... paths) { Preconditions.checkArgument(paths != null && paths.length > 0, "StackablePath cannot be null or empty"); List<History.Entry> entries = new ArrayList<>(paths.length); for (int i = 0; i < paths.length; i++) { entries.add(history.add(paths[i], navType)); } return entries; } private History.Entry add(int navType, StackablePath path) { Preconditions.checkNotNull(path, "StackablePath cannot be null"); return history.add(path, navType); } private void check() { Preconditions.checkNotNull(scope, "Navigator scope cannot be null"); } /** * Scope can be null if the method is called after the navigator scope was destroyed * //TODO: is it really possible to be null? */ MortarScope getScope() { return scope; } public NavigatorLifecycleDelegate delegate() { return delegate; } public Transitions transitions() { return transitions; } // Scoped @Override public void onEnterScope(MortarScope scope) { Preconditions.checkNull(this.scope, "Cannot register navigator multiple times in a scope"); this.scope = scope; } /** * Scope associated to navigator is destroyed * Everything will be destroyed */ @Override public void onExitScope() { Logger.d("Navigation scope exit"); // stop and kill the dispatcher dispatcher.kill(); scope = null; } public static class Config { /** * After process kill, the previous stack won't be restored * and the app will start from the beginning again * The advantage of this is that it allows the developer to not care * about saving and restoring state in presenters' bundles at all * Default value is false, the stack will be restored */ boolean dontRestoreStackAfterKill; public Config dontRestoreStackAfterKill(boolean dontRestoreStackAfterKill) { this.dontRestoreStackAfterKill = dontRestoreStackAfterKill; return this; } } }