Compare commits

...

10 Commits

15 changed files with 437 additions and 104 deletions
+9 -8
View File
@@ -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()));
}
@@ -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
}
}
@@ -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)
}
}
}
@@ -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(),
@@ -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()))
}
@@ -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
View File
@@ -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
View File
@@ -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