Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8ac58ad6a | |||
| 5f04d9de89 | |||
| d32fc813d0 | |||
| c2bc72c5ce | |||
| 924e4bebfa | |||
| 4ea4aa5c56 | |||
| 3b275d31c2 | |||
| 0e21c8c9c1 | |||
| 8297e0273d | |||
| 46519c2c2c |
@@ -20,27 +20,27 @@ Conductor is architecture-agnostic and does not try to force any design decision
|
||||
## Installation
|
||||
|
||||
```gradle
|
||||
implementation 'com.bluelinelabs:conductor:3.1.1'
|
||||
implementation 'com.bluelinelabs:conductor:3.1.5'
|
||||
|
||||
// AndroidX Transition change handlers:
|
||||
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.1'
|
||||
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.5'
|
||||
|
||||
// ViewPager PagerAdapter:
|
||||
implementation 'com.bluelinelabs:conductor-viewpager:3.1.1'
|
||||
implementation 'com.bluelinelabs:conductor-viewpager:3.1.5'
|
||||
|
||||
// ViewPager2 Adapter:
|
||||
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.1'
|
||||
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.5'
|
||||
|
||||
// RxJava2 Autodispose support:
|
||||
implementation 'com.bluelinelabs:conductor-autodispose:3.1.1'
|
||||
implementation 'com.bluelinelabs:conductor-autodispose:3.1.5'
|
||||
|
||||
// Lifecycle-aware Controllers (architecture components):
|
||||
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.1'
|
||||
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.5'
|
||||
```
|
||||
|
||||
**SNAPSHOT**
|
||||
|
||||
Just use `3.1.2-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
|
||||
Just use `3.1.6-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
|
||||
|
||||
```gradle
|
||||
allprojects {
|
||||
@@ -76,7 +76,8 @@ public class MainActivity extends Activity {
|
||||
|
||||
ViewGroup container = (ViewGroup) findViewById(R.id.controller_container);
|
||||
|
||||
router = Conductor.attachRouter(this, container, savedInstanceState);
|
||||
router = Conductor.attachRouter(this, container, savedInstanceState)
|
||||
.setPopRootControllerMode(PopRootControllerMode.NEVER);
|
||||
if (!router.hasRootController()) {
|
||||
router.setRoot(RouterTransaction.with(new HomeController()));
|
||||
}
|
||||
|
||||
+1
-1
@@ -154,7 +154,7 @@ abstract class RouterStateAdapter(private val host: Controller) :
|
||||
|
||||
private fun attachRouter(holder: RouterViewHolder, position: Int) {
|
||||
val itemId = getItemId(position)
|
||||
val router = host.getChildRouter(holder.container, "$itemId")
|
||||
val router = host.getChildRouter(holder.container, "$itemId", true, false)!!
|
||||
|
||||
// This should have already been handled by onViewRecycled, but it seems like this wasn't
|
||||
// always reliably called
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -215,6 +214,23 @@ public abstract class Controller {
|
||||
*/
|
||||
@Nullable
|
||||
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded) {
|
||||
return getChildRouter(container, tag, createIfNeeded, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the child {@link Router} for the given container/tag combination. Note that multiple
|
||||
* routers should not exist in the same container unless a lot of care is taken to maintain order
|
||||
* between them. Avoid using the same container unless you have a great reason to do so (ex: ViewPagers).
|
||||
* The only time this method will return {@code null} is when the child router does not exist prior
|
||||
* to calling this method and the createIfNeeded parameter is set to false.
|
||||
*
|
||||
* @param container The ViewGroup that hosts the child Router
|
||||
* @param tag The router's tag or {@code null} if none is needed
|
||||
* @param createIfNeeded If true, a router will be created if one does not yet exist. Else {@code null} will be returned in this case.
|
||||
* @param boundToHostContainerId If true, a router will only ever rebind with a container with the same view id on state restoration. Note that this must be set to true if the tag is null.
|
||||
*/
|
||||
@Nullable
|
||||
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded, boolean boundToHostContainerId) {
|
||||
@IdRes final int containerId = container.getId();
|
||||
if (containerId == View.NO_ID) {
|
||||
throw new IllegalStateException("You must set an id on your container.");
|
||||
@@ -222,7 +238,7 @@ public abstract class Controller {
|
||||
|
||||
ControllerHostedRouter childRouter = null;
|
||||
for (ControllerHostedRouter router : childRouters) {
|
||||
if (router.getHostId() == containerId && TextUtils.equals(tag, router.getTag())) {
|
||||
if (router.matches(containerId, tag)) {
|
||||
childRouter = router;
|
||||
break;
|
||||
}
|
||||
@@ -230,7 +246,7 @@ public abstract class Controller {
|
||||
|
||||
if (childRouter == null) {
|
||||
if (createIfNeeded) {
|
||||
childRouter = new ControllerHostedRouter(container.getId(), tag);
|
||||
childRouter = new ControllerHostedRouter(container.getId(), tag, boundToHostContainerId);
|
||||
childRouter.setHostContainer(this, container);
|
||||
childRouters.add(childRouter);
|
||||
|
||||
@@ -856,6 +872,27 @@ public abstract class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
final void onContextUnavailable(@NonNull Context context) {
|
||||
if (isContextAvailable) {
|
||||
for (Router childRouter : childRouters) {
|
||||
childRouter.onContextUnavailable(context);
|
||||
}
|
||||
|
||||
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.preContextUnavailable(this, context);
|
||||
}
|
||||
|
||||
isContextAvailable = false;
|
||||
onContextUnavailable();
|
||||
|
||||
listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.postContextUnavailable(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final void executeWithRouter(@NonNull RouterRequiringFunc listener) {
|
||||
if (router != null) {
|
||||
listener.execute();
|
||||
@@ -908,20 +945,7 @@ public abstract class Controller {
|
||||
destroy(true);
|
||||
}
|
||||
|
||||
if (isContextAvailable) {
|
||||
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.preContextUnavailable(this, activity);
|
||||
}
|
||||
|
||||
isContextAvailable = false;
|
||||
onContextUnavailable();
|
||||
|
||||
listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.postContextUnavailable(this);
|
||||
}
|
||||
}
|
||||
onContextUnavailable(activity);
|
||||
}
|
||||
|
||||
void attach(@NonNull View view) {
|
||||
@@ -981,27 +1005,30 @@ public abstract class Controller {
|
||||
final boolean removeViewRef = !blockViewRefRemoval && (forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed);
|
||||
|
||||
if (attached) {
|
||||
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.preDetach(this, view);
|
||||
}
|
||||
|
||||
attached = false;
|
||||
|
||||
if (!awaitingParentAttach) {
|
||||
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.preDetach(this, view);
|
||||
}
|
||||
|
||||
attached = false;
|
||||
onDetach(view);
|
||||
}
|
||||
|
||||
if (hasOptionsMenu && !optionsMenuHidden) {
|
||||
router.invalidateOptionsMenu();
|
||||
}
|
||||
if (hasOptionsMenu && !optionsMenuHidden) {
|
||||
router.invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.postDetach(this, view);
|
||||
listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.postDetach(this, view);
|
||||
}
|
||||
} else {
|
||||
attached = false;
|
||||
}
|
||||
}
|
||||
|
||||
awaitingParentAttach = false;
|
||||
|
||||
if (removeViewRef) {
|
||||
removeViewReference();
|
||||
}
|
||||
@@ -1123,18 +1150,7 @@ public abstract class Controller {
|
||||
|
||||
private void performDestroy() {
|
||||
if (isContextAvailable) {
|
||||
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.preContextUnavailable(this, getActivity());
|
||||
}
|
||||
|
||||
isContextAvailable = false;
|
||||
onContextUnavailable();
|
||||
|
||||
listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.postContextUnavailable(this);
|
||||
}
|
||||
onContextUnavailable(getActivity());
|
||||
}
|
||||
|
||||
if (!destroyed) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.content.IntentSender.SendIntentException;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
@@ -22,18 +23,24 @@ class ControllerHostedRouter extends Router {
|
||||
|
||||
private final String KEY_HOST_ID = "ControllerHostedRouter.hostId";
|
||||
private final String KEY_TAG = "ControllerHostedRouter.tag";
|
||||
private final String KEY_BOUND_TO_CONTAINER = "ControllerHostedRouter.boundToContainer";
|
||||
|
||||
private Controller hostController;
|
||||
|
||||
@IdRes private int hostId;
|
||||
private String tag;
|
||||
private boolean isDetachFrozen;
|
||||
private boolean boundToContainer;
|
||||
|
||||
ControllerHostedRouter() { }
|
||||
|
||||
ControllerHostedRouter(int hostId, @Nullable String tag) {
|
||||
ControllerHostedRouter(int hostId, @Nullable String tag, boolean boundToContainer) {
|
||||
if (!boundToContainer && tag == null) {
|
||||
throw new IllegalStateException("ControllerHostedRouter can't be created without a tag if not bounded to its container");
|
||||
}
|
||||
this.hostId = hostId;
|
||||
this.tag = tag;
|
||||
this.boundToContainer = boundToContainer;
|
||||
}
|
||||
|
||||
final void setHostController(@NonNull Controller controller) {
|
||||
@@ -213,6 +220,7 @@ class ControllerHostedRouter extends Router {
|
||||
super.saveInstanceState(outState);
|
||||
|
||||
outState.putInt(KEY_HOST_ID, hostId);
|
||||
outState.putBoolean(KEY_BOUND_TO_CONTAINER, boundToContainer);
|
||||
outState.putString(KEY_TAG, tag);
|
||||
}
|
||||
|
||||
@@ -221,6 +229,7 @@ class ControllerHostedRouter extends Router {
|
||||
super.restoreInstanceState(savedInstanceState);
|
||||
|
||||
hostId = savedInstanceState.getInt(KEY_HOST_ID);
|
||||
boundToContainer = savedInstanceState.getBoolean(KEY_BOUND_TO_CONTAINER);
|
||||
tag = savedInstanceState.getString(KEY_TAG);
|
||||
}
|
||||
|
||||
@@ -234,9 +243,18 @@ class ControllerHostedRouter extends Router {
|
||||
return hostId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getTag() {
|
||||
return tag;
|
||||
boolean matches(int hostId, @Nullable String tag) {
|
||||
if (!boundToContainer && container == null) {
|
||||
if (this.tag == null) {
|
||||
throw new IllegalStateException("Host ID can't be variable with a null tag");
|
||||
}
|
||||
if (this.tag.equals(tag)) {
|
||||
this.hostId = hostId;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return this.hostId == hostId && TextUtils.equals(tag, this.tag);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.os.Bundle;
|
||||
@@ -34,14 +35,14 @@ import java.util.List;
|
||||
public abstract class Router {
|
||||
|
||||
private static final String KEY_BACKSTACK = "Router.backstack";
|
||||
private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView";
|
||||
private static final String KEY_POP_ROOT_CONTROLLER_MODE = "Router.popRootControllerMode";
|
||||
|
||||
final Backstack backstack = new Backstack();
|
||||
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
|
||||
private final List<ChangeTransaction> pendingControllerChanges = new ArrayList<>();
|
||||
final List<Controller> destroyingControllers = new ArrayList<>();
|
||||
|
||||
private boolean popsLastView = false;
|
||||
private PopRootControllerMode popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
|
||||
boolean containerFullyAttached = false;
|
||||
boolean isActivityStopped = false;
|
||||
|
||||
@@ -94,7 +95,7 @@ public abstract class Router {
|
||||
//noinspection ConstantConditions
|
||||
if (backstack.peek().controller().handleBack()) {
|
||||
return true;
|
||||
} else if (popCurrentController()) {
|
||||
} else if ((backstack.getSize() > 1 || popRootControllerMode != PopRootControllerMode.NEVER) && popCurrentController()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -161,7 +162,7 @@ public abstract class Router {
|
||||
}
|
||||
}
|
||||
|
||||
if (popsLastView) {
|
||||
if (popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW) {
|
||||
return topTransaction != null;
|
||||
} else {
|
||||
return !backstack.isEmpty();
|
||||
@@ -220,7 +221,7 @@ public abstract class Router {
|
||||
}
|
||||
|
||||
void destroy(boolean popViews) {
|
||||
popsLastView = true;
|
||||
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW;
|
||||
final List<RouterTransaction> poppedControllers = backstack.popAll();
|
||||
trackDestroyingControllers(poppedControllers);
|
||||
|
||||
@@ -250,10 +251,24 @@ public abstract class Router {
|
||||
* If set to true, this router will handle back presses by performing a change handler on the last controller and view
|
||||
* in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise
|
||||
* hide its parent view without any strange artifacting.
|
||||
*
|
||||
* Note: This method has been deprecated and should be replaced with setPopRootControllerMode.
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public Router setPopsLastView(boolean popsLastView) {
|
||||
this.popsLastView = popsLastView;
|
||||
this.popRootControllerMode = popsLastView ? PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW : PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the method this router will use to handle back presses when there is only one controller left in the backstack.
|
||||
* Defaults to POP_ROOT_CONTROLLER_LEAVING_VIEW so that the developer can either finish its containing Activity or
|
||||
* otherwise hide its parent view without any strange artifacting.
|
||||
*/
|
||||
@NonNull
|
||||
public Router setPopRootControllerMode(@NonNull PopRootControllerMode popRootControllerMode) {
|
||||
this.popRootControllerMode = popRootControllerMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -649,14 +664,14 @@ public abstract class Router {
|
||||
backstack.saveInstanceState(backstackState);
|
||||
|
||||
outState.putParcelable(KEY_BACKSTACK, backstackState);
|
||||
outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView);
|
||||
outState.putInt(KEY_POP_ROOT_CONTROLLER_MODE, popRootControllerMode.ordinal());
|
||||
}
|
||||
|
||||
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
|
||||
//noinspection ConstantConditions
|
||||
backstack.restoreInstanceState(backstackBundle);
|
||||
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
|
||||
popRootControllerMode = PopRootControllerMode.values()[savedInstanceState.getInt(KEY_POP_ROOT_CONTROLLER_MODE)];
|
||||
|
||||
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
|
||||
while (backstackIterator.hasNext()) {
|
||||
@@ -727,6 +742,7 @@ public abstract class Router {
|
||||
@Override
|
||||
public void run() {
|
||||
containerFullyAttached = true;
|
||||
performPendingControllerChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -745,6 +761,15 @@ public abstract class Router {
|
||||
}
|
||||
}
|
||||
|
||||
void onContextUnavailable(@NonNull Context context) {
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
transaction.controller().onContextUnavailable(context);
|
||||
}
|
||||
for (Controller controller : destroyingControllers) {
|
||||
controller.onContextUnavailable(context);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
final List<Controller> getControllers() {
|
||||
List<Controller> controllers = new ArrayList<>(backstack.getSize());
|
||||
@@ -793,7 +818,7 @@ public abstract class Router {
|
||||
if (to != null) {
|
||||
to.ensureValidIndex(getTransactionIndexer());
|
||||
setRouterOnController(toController);
|
||||
} else if (backstack.getSize() == 0 && !popsLastView) {
|
||||
} else if (backstack.getSize() == 0 && popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW) {
|
||||
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The host
|
||||
// Activity or controller should be handling this by finishing or at least hiding this view.
|
||||
changeHandler = new NoOpControllerChangeHandler();
|
||||
@@ -837,12 +862,14 @@ public abstract class Router {
|
||||
to.setNeedsAttach(true);
|
||||
}
|
||||
pendingControllerChanges.add(transaction);
|
||||
container.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
performPendingControllerChanges();
|
||||
}
|
||||
});
|
||||
if (container != null) {
|
||||
container.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
performPendingControllerChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ControllerChangeHandler.executeChange(transaction);
|
||||
}
|
||||
@@ -1001,4 +1028,25 @@ public abstract class Router {
|
||||
@NonNull abstract Router getRootRouter();
|
||||
@NonNull abstract TransactionIndexer getTransactionIndexer();
|
||||
|
||||
/**
|
||||
* Defines the way a Router will handle back button or pop events when there is only one controller
|
||||
* left in the backstack.
|
||||
*/
|
||||
public enum PopRootControllerMode {
|
||||
/**
|
||||
* The Router will not pop the final controller left on the backstack when the back button is pressed
|
||||
* or when pop events are called. This mode should generally be used for Activity-hosted routers.
|
||||
*/
|
||||
NEVER,
|
||||
/**
|
||||
* The Router will pop the final controller, but will leave its view in the hierarchy. This is useful
|
||||
* when the developer wishes to allow its containing Activity to finish or otherwise hide its parent
|
||||
* view without any strange artifacting.
|
||||
*/
|
||||
POP_ROOT_CONTROLLER_BUT_NOT_VIEW,
|
||||
/**
|
||||
* The Router will pop both the final controller as well as its view.
|
||||
*/
|
||||
POP_ROOT_CONTROLLER_AND_VIEW
|
||||
}
|
||||
}
|
||||
|
||||
+77
-21
@@ -1,5 +1,6 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -28,6 +29,7 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
|
||||
private var hasSavedState = false
|
||||
private var savedRegistryState = Bundle.EMPTY
|
||||
private val parentChangeListeners = mutableMapOf<String, Controller.LifecycleListener>()
|
||||
|
||||
init {
|
||||
controller.addLifecycleListener(object : Controller.LifecycleListener() {
|
||||
@@ -84,29 +86,17 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
|
||||
// does this on init, its detach callbacks get called before ours, which prevents us
|
||||
// from saving state in onDetach. The if statement in here should detect upcoming
|
||||
// detachment.
|
||||
override fun onChangeStart(
|
||||
changeController: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
changeType: ControllerChangeType,
|
||||
) {
|
||||
if (
|
||||
controller === changeController &&
|
||||
!changeType.isEnter &&
|
||||
changeHandler.removesFromViewOnPush() &&
|
||||
changeController.view != null &&
|
||||
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
|
||||
) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
|
||||
savedRegistryState = Bundle()
|
||||
savedStateRegistryController.performSave(savedRegistryState)
|
||||
|
||||
hasSavedState = true
|
||||
}
|
||||
pauseOnChangeStart(
|
||||
targetController = controller,
|
||||
changeController = changeController,
|
||||
changeHandler = changeHandler,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
@@ -147,6 +137,14 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun postContextAvailable(controller: Controller, context: Context) {
|
||||
listenForParentChangeStart(controller)
|
||||
}
|
||||
|
||||
override fun preContextUnavailable(controller: Controller, context: Context) {
|
||||
stopListeningForParentChangeStart(controller)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -154,11 +152,69 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
|
||||
override fun getSavedStateRegistry() = savedStateRegistryController.savedStateRegistry
|
||||
|
||||
private fun listenForParentChangeStart(controller: Controller) {
|
||||
controller.parentController?.let { parent ->
|
||||
val changeListener = object : Controller.LifecycleListener() {
|
||||
override fun onChangeStart(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
pauseOnChangeStart(
|
||||
targetController = parent,
|
||||
changeController = controller,
|
||||
changeHandler = changeHandler,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
parent.addLifecycleListener(changeListener)
|
||||
parentChangeListeners[controller.instanceId] = changeListener
|
||||
|
||||
listenForParentChangeStart(parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopListeningForParentChangeStart(controller: Controller) {
|
||||
controller.parentController?.let { parent ->
|
||||
parentChangeListeners.remove(parent.instanceId)?.let { listener ->
|
||||
parent.removeLifecycleListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
|
||||
// does this on init, its detach callbacks get called before ours, which prevents us
|
||||
// from saving state in onDetach. The if statement in here should detect upcoming
|
||||
// detachment.
|
||||
private fun pauseOnChangeStart(
|
||||
targetController: Controller,
|
||||
changeController: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType,
|
||||
) {
|
||||
if (
|
||||
targetController === changeController &&
|
||||
!changeType.isEnter &&
|
||||
changeHandler.removesFromViewOnPush() &&
|
||||
changeController.view != null &&
|
||||
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
|
||||
) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
|
||||
savedRegistryState = Bundle()
|
||||
savedStateRegistryController.performSave(savedRegistryState)
|
||||
|
||||
hasSavedState = true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_SAVED_STATE = "Registry.savedState"
|
||||
|
||||
fun own(target: Controller) {
|
||||
OwnViewTreeLifecycleAndRegistry(target)
|
||||
fun own(target: Controller): OwnViewTreeLifecycleAndRegistry {
|
||||
return OwnViewTreeLifecycleAndRegistry(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -112,7 +112,7 @@ class ControllerLifecycleActivityReferenceTests {
|
||||
child.addLifecycleListener(listener)
|
||||
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopsLastView(true)
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
@@ -146,7 +146,7 @@ class ControllerLifecycleActivityReferenceTests {
|
||||
child.addLifecycleListener(listener)
|
||||
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopsLastView(true)
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
@@ -199,7 +199,7 @@ class ControllerLifecycleActivityReferenceTests {
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
child.addLifecycleListener(listener)
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopsLastView(true)
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
|
||||
@@ -276,7 +276,7 @@ class ControllerTests {
|
||||
Assert.assertNull(child2.parentController)
|
||||
|
||||
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopsLastView(true)
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.setRoot(child1.asTransaction())
|
||||
Assert.assertEquals(1, parent.childRouters.size)
|
||||
Assert.assertEquals(childRouter, parent.childRouters[0])
|
||||
@@ -362,7 +362,7 @@ class ControllerTests {
|
||||
val childTransaction1 = TestController().asTransaction()
|
||||
val childTransaction2 = TestController().asTransaction()
|
||||
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
|
||||
childRouter.setPopsLastView(true)
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.setRoot(childTransaction1)
|
||||
childRouter.pushController(childTransaction2)
|
||||
val savedState = Bundle()
|
||||
|
||||
@@ -124,7 +124,7 @@ class ReattachCaseTests {
|
||||
val childRouter = controllerB.getChildRouter(
|
||||
controllerB.view!!.findViewById(TestController.VIEW_ID)
|
||||
)
|
||||
childRouter.setPopsLastView(true)
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
childController.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
@@ -179,7 +179,7 @@ class ReattachCaseTests {
|
||||
val childRouter = controllerB.getChildRouter(
|
||||
controllerB.view!!.findViewById(TestController.VIEW_ID)
|
||||
)
|
||||
childRouter.setPopsLastView(true)
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
childController.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.TestController
|
||||
import com.bluelinelabs.conductor.asTransaction
|
||||
import com.bluelinelabs.conductor.util.TestActivity
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE)
|
||||
class OwnViewTreeLifecycleAndRegistryTest {
|
||||
|
||||
private val router = Robolectric.buildActivity(TestActivity::class.java)
|
||||
.setup()
|
||||
.get()
|
||||
.router
|
||||
|
||||
@Test
|
||||
fun `onCreate lifecycle event before create view`() {
|
||||
assertControllerState(
|
||||
preCreateViewAssertedState = Lifecycle.State.CREATED,
|
||||
setup = { router.setRoot(it.asTransaction()) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onStart lifecycle event after create view`() {
|
||||
assertControllerState(
|
||||
postCreateViewAssertedState = Lifecycle.State.STARTED,
|
||||
setup = { router.setRoot(it.asTransaction()) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onResume lifecycle event on attach`() {
|
||||
assertControllerState(
|
||||
postAttachAssertedState = Lifecycle.State.RESUMED,
|
||||
setup = { router.setRoot(it.asTransaction()) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onPause lifecycle event on exit change start`() {
|
||||
assertControllerState(
|
||||
onChangeStartAssertedState = Lifecycle.State.STARTED,
|
||||
setup = {
|
||||
router.setRoot(it.asTransaction())
|
||||
router.pushController(TestController().asTransaction())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onPause lifecycle event on parent exit change start`() {
|
||||
val parent = TestController()
|
||||
val controller = TestController()
|
||||
var hasAsserted = false
|
||||
val ownViewTreeLifecycleAndRegistry = OwnViewTreeLifecycleAndRegistry.own(controller)
|
||||
|
||||
// Ensure our listener gets added after OwnViewTreeLifecycleAndRegistry's by waiting until
|
||||
// postContextAvailable to add the lifecycle listener on the parent controller
|
||||
controller.addLifecycleListener(object : Controller.LifecycleListener() {
|
||||
override fun postContextAvailable(controller: Controller, context: Context) {
|
||||
parent.addLifecycleListener(object : Controller.LifecycleListener() {
|
||||
override fun onChangeStart(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
Assert.assertEquals(Lifecycle.State.STARTED, ownViewTreeLifecycleAndRegistry.lifecycle.currentState)
|
||||
hasAsserted = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
router.setRoot(parent.asTransaction())
|
||||
parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID)).setRoot(controller.asTransaction())
|
||||
router.pushController(TestController().asTransaction())
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
|
||||
Assert.assertTrue(hasAsserted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onStop lifecycle event on detach`() {
|
||||
assertControllerState(
|
||||
preDetachAssertedState = Lifecycle.State.CREATED,
|
||||
setup = {
|
||||
router.setRoot(it.asTransaction())
|
||||
router.pushController(TestController().asTransaction())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onDestroy lifecycle event on destroy view`() {
|
||||
assertControllerState(
|
||||
preDestroyViewAssertedState = Lifecycle.State.DESTROYED,
|
||||
setup = {
|
||||
router.setRoot(it.asTransaction())
|
||||
router.pushController(TestController().asTransaction())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertControllerState(
|
||||
preCreateViewAssertedState: Lifecycle.State? = null,
|
||||
postCreateViewAssertedState: Lifecycle.State? = null,
|
||||
postAttachAssertedState: Lifecycle.State? = null,
|
||||
preDetachAssertedState: Lifecycle.State? = null,
|
||||
preDestroyViewAssertedState: Lifecycle.State? = null,
|
||||
onChangeStartAssertedState: Lifecycle.State? = null,
|
||||
setup: (Controller) -> Unit = { },
|
||||
) {
|
||||
val controller = TestController()
|
||||
val ownViewTreeLifecycleAndRegistry = OwnViewTreeLifecycleAndRegistry.own(controller)
|
||||
var hasAsserted = false
|
||||
|
||||
val assertState: (Lifecycle.State) -> Unit = {
|
||||
Assert.assertEquals(it, ownViewTreeLifecycleAndRegistry.lifecycle.currentState)
|
||||
}
|
||||
|
||||
controller.addLifecycleListener(object : Controller.LifecycleListener() {
|
||||
override fun preCreateView(controller: Controller) {
|
||||
preCreateViewAssertedState?.let {
|
||||
assertState(it)
|
||||
hasAsserted = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
postCreateViewAssertedState?.let {
|
||||
assertState(it)
|
||||
hasAsserted = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun postAttach(controller: Controller, view: View) {
|
||||
postAttachAssertedState?.let {
|
||||
assertState(it)
|
||||
hasAsserted = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
preDetachAssertedState?.let {
|
||||
assertState(it)
|
||||
hasAsserted = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
preDestroyViewAssertedState?.let {
|
||||
assertState(it)
|
||||
hasAsserted = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeStart(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
if (!changeType.isEnter) {
|
||||
onChangeStartAssertedState?.let {
|
||||
assertState(it)
|
||||
hasAsserted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setup(controller)
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
Assert.assertTrue(hasAsserted)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.bluelinelabs.conductor.Conductor.attachRouter
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.Router.PopRootControllerMode
|
||||
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
|
||||
import com.bluelinelabs.conductor.demo.controllers.HomeController
|
||||
import com.bluelinelabs.conductor.demo.databinding.ActivityMainBinding
|
||||
@@ -23,6 +24,8 @@ class MainActivity : AppCompatActivity(), ToolbarProvider {
|
||||
setContentView(binding.root)
|
||||
|
||||
router = attachRouter(this, binding.controllerContainer, savedInstanceState)
|
||||
.setPopRootControllerMode(PopRootControllerMode.NEVER)
|
||||
|
||||
if (!router.hasRootController()) {
|
||||
router.setRoot(with(HomeController()))
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.view.View
|
||||
import com.bluelinelabs.conductor.Router.PopRootControllerMode
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
|
||||
@@ -18,7 +19,7 @@ class MultipleChildRouterController : BaseController(R.layout.controller_multipl
|
||||
val childContainers = listOf(binding.container0, binding.container1, binding.container2)
|
||||
|
||||
childContainers.forEach { container ->
|
||||
val childRouter = getChildRouter(container).setPopsLastView(false)
|
||||
val childRouter = getChildRouter(container).setPopRootControllerMode(PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW)
|
||||
if (!childRouter.hasRootController()) {
|
||||
childRouter.setRoot(RouterTransaction.with(NavigationDemoController(0, NavigationDemoController.DisplayUpMode.HIDE)))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bluelinelabs.conductor.demo.controllers
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.Router.PopRootControllerMode
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
@@ -11,7 +12,6 @@ import com.bluelinelabs.conductor.demo.databinding.ControllerParentBinding
|
||||
import com.bluelinelabs.conductor.demo.util.getMaterialColor
|
||||
import com.bluelinelabs.conductor.demo.util.viewBinding
|
||||
|
||||
|
||||
class ParentController : BaseController(R.layout.controller_parent) {
|
||||
private val binding: ControllerParentBinding by viewBinding(ControllerParentBinding::bind)
|
||||
|
||||
@@ -50,7 +50,7 @@ class ParentController : BaseController(R.layout.controller_parent) {
|
||||
else -> throw IllegalStateException("Invalid child index $index")
|
||||
}
|
||||
|
||||
val childRouter = getChildRouter(container).setPopsLastView(true)
|
||||
val childRouter = getChildRouter(container).setPopRootControllerMode(PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
if (!childRouter.hasRootController()) {
|
||||
val child = ChildController(
|
||||
title = "Child Controller #$index",
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ ext {
|
||||
dokkaVersion = '1.4.32'
|
||||
composeVersion = "1.0.0-beta09"
|
||||
|
||||
agpVersion = "7.0.0-beta05"
|
||||
agpVersion = '7.0.3'
|
||||
lintVersion = agpVersion.replaceFirst(~/\d*/) { version ->
|
||||
// the major version of lint is always 23 version higher than the major version of agp
|
||||
version.toInteger() + 23
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
VERSION_CODE=3
|
||||
VERSION_NAME=3.1.2-SNAPSHOT
|
||||
VERSION_NAME=3.1.6-SNAPSHOT
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
|
||||
Reference in New Issue
Block a user