Compare commits

...

31 Commits

Author SHA1 Message Date
EricKuck c3f7d128f5 Version bump 2023-01-30 11:56:22 -05:00
EricKuck 5e1f072672 Update maven publishing 2023-01-27 10:38:57 -05:00
EricKuck b0d15d9f9e Update maven publishing 2023-01-27 10:26:15 -05:00
Eric Kuck 8ac2e04c62 Make change start/end callbacks when parent controller is popped. Fixes #683 (#684) 2023-01-26 10:11:33 -06:00
Eric Kuck cdbdee5c42 Don't pop the final controller in pager adapters. Fixes #681 (#682) 2023-01-26 09:57:39 -06:00
EricKuck 8488242a26 Version bump 2022-11-29 15:04:22 -05:00
Eric Kuck 1fe0187439 Update ancestor change listeners to prevent memory leak (#680) 2022-11-29 13:51:25 -06:00
EricKuck f78726b916 Version bump 2022-11-07 10:38:21 -05:00
EricKuck 1f918f10c5 Fix ControllerLifecycleOwner crash when onContextAvailable was never called 2022-11-03 15:29:15 -04:00
EricKuck bd584727be Fix edge case ConcurrentModificationException 2022-09-19 16:10:39 -04:00
EricKuck 91db7fe65f Capture view reference in inflate call 2022-07-28 11:29:02 -05:00
EricKuck 2abe2b33f9 Version bump 2022-07-28 09:41:02 -05:00
Mario Noll ac4e09cf67 Fix NPE when removing view reference (#678)
Great catch, thanks!
2022-07-28 08:59:29 -05:00
EricKuck 055532bb21 Fix github actions badge 2022-07-25 17:17:08 -05:00
EricKuck 15037c2217 Version bump 2022-07-25 16:48:26 -05:00
EricKuck 728f1fb4e9 Ensure onContextUnavailable called on child routers for edge case 2022-07-22 11:55:15 -05:00
EricKuck 55c8d64d8a Fix CI badge 2022-07-05 14:46:14 -05:00
EricKuck 88e0eb882b Fix crash when a parent is transitioned out before a child can create its view 2022-07-05 10:43:52 -05:00
EricKuck 63a92db540 Pass along View's context on destroy if available 2022-07-05 10:43:06 -05:00
py - Pierre Yves Ricau ba98e3b165 Add leak detection for destroyed controller views (#676) 2022-06-23 12:00:39 -05:00
Eric Kuck 966bc1645d Convert dependencies to version catalogs (#675)
Also bumps gradle, agp, and kotlin versions
2022-06-16 16:40:05 -05:00
EricKuck c8ac58ad6a Version bump 2022-06-14 13:44:57 -05:00
Eric Kuck 5f04d9de89 Fix edge case NPE when the user hits the back button very rapidly during state restoration (#674) 2022-06-14 10:12:44 -05:00
Eric Kuck d32fc813d0 Set awaitingParentAttach to false on detach, even if not currently attached (#673) 2022-06-13 18:09:55 -05:00
Eric Kuck c2bc72c5ce Fix issue where child controllers may not get their onPause event soon enough (#672) 2022-06-13 18:09:42 -05:00
EricKuck 924e4bebfa Version bump 2022-02-18 14:28:27 -06:00
EricKuck 4ea4aa5c56 Fix issue with detach callbacks happening while not yet attached 2022-02-18 14:15:05 -06:00
Eric Kuck 3b275d31c2 Add PopRootControllerMode to address Android 12 back button behavior (#663) 2022-01-27 16:41:20 -06:00
EricKuck 0e21c8c9c1 Version bump 2021-11-30 11:39:30 -06:00
EricKuck 8297e0273d RouterStateAdapter now properly restores routers after process death 2021-11-28 11:27:47 -06:00
EricKuck 46519c2c2c Ensure preContextUnavailable can't use a null context in the parameter
Fixes #377 and #516
2021-11-23 15:50:52 -06:00
32 changed files with 713 additions and 287 deletions
+4 -4
View File
@@ -31,8 +31,8 @@ jobs:
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew clean uploadArchives
- name: Release with Gradle
run: ./gradlew clean publishAllPublicationsToMavenCentral
env:
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
+3 -1
View File
@@ -8,7 +8,9 @@
*.iml
*.ipr
*.iws
*.idea/dictionaries
/.idea/*
!/.idea/codeStyles/
!/.idea/scopes/
classes
gen-external-apklibs
+12 -9
View File
@@ -1,4 +1,4 @@
[![Travis Build](https://travis-ci.com/bluelinelabs/Conductor.svg)](https://travis-ci.com/bluelinelabs/Conductor) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Conductor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3361) [![Javadocs](http://javadoc.io/badge/com.bluelinelabs/conductor.svg)](http://javadoc.io/doc/com.bluelinelabs/conductor)
[![GitHub Actions Workflow](https://github.com/bluelinelabs/conductor/actions/workflows/main.yml/badge.svg)](https://github.com/bluelinelabs/conductor/actions/workflows/main.yml) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Conductor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3361) [![Javadocs](http://javadoc.io/badge/com.bluelinelabs/conductor.svg)](http://javadoc.io/doc/com.bluelinelabs/conductor)
# Conductor
@@ -20,27 +20,29 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
implementation 'com.bluelinelabs:conductor:3.1.1'
def conductorVersion = '3.2.0'
implementation "com.bluelinelabs:conductor:$conductorVersion"
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.1'
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.1.1'
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
// ViewPager2 Adapter:
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.1'
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.1.1'
implementation "com.bluelinelabs:conductor-autodispose:$conductorVersion"
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.1'
implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
```
**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.2.1-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
```gradle
allprojects {
@@ -76,7 +78,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()));
}
+19 -8
View File
@@ -1,15 +1,12 @@
buildscript {
apply from: rootProject.file('dependencies.gradle')
repositories {
mavenCentral()
google()
}
dependencies {
classpath "com.android.tools.build:gradle:$agpVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.vanniktech:gradle-maven-publish-plugin:$mvnPublishVersion"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion"
classpath libs.agp
classpath libs.kotlin.plugin
classpath libs.dokka
}
}
@@ -18,6 +15,20 @@ allprojects {
mavenCentral()
google()
}
}
apply from: rootProject.file('dependencies.gradle')
if (project.hasProperty('maven_publish_url')) {
pluginManager.withPlugin("com.vanniktech.maven.publish") {
publishing {
repositories {
maven {
url = maven_publish_url
credentials {
username = maven_publish_username
password = maven_publish_password
}
}
}
}
}
}
}
+7 -9
View File
@@ -1,17 +1,17 @@
apply plugin: 'kotlin'
configurations {
lintChecks
libs.lint.checks
}
dependencies {
compileOnly rootProject.ext.lintapi
compileOnly rootProject.ext.lintchecks
compileOnly rootProject.ext.kotlinStd
compileOnly libs.lint.api
compileOnly libs.lint.checks
compileOnly libs.kotlin.stdlib
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.lint
testImplementation rootProject.ext.lintTests
testImplementation libs.junit
testImplementation libs.lint
testImplementation libs.lint.tests
}
jar {
@@ -19,5 +19,3 @@ jar {
attributes('Lint-Registry-v2': 'com.bluelinelabs.conductor.lint.IssueRegistry')
}
}
apply from: rootProject.file('dependencies.gradle')
@@ -1,24 +1,24 @@
apply plugin: 'com.android.library'
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxCollection
api rootProject.ext.androidxTransition
implementation libs.androidx.appcompat
implementation libs.androidx.collection
api libs.androidx.transition
implementation project(':conductor')
}
ext.artifactId = 'conductor-androidx-transition'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -1,23 +1,23 @@
apply plugin: 'com.android.library'
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.archComponentsLifecycle
api libs.androidx.lifecycle.runtime
implementation project(':conductor')
}
ext.artifactId = 'conductor-arch-components-lifecycle'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -51,7 +51,10 @@ public class ControllerLifecycleOwner implements LifecycleOwner {
@Override
public void preDestroy(@NonNull Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
// Only act on Controllers that have had at least the onContextAvailable call made on them.
if (lifecycleRegistry.getCurrentState() != Lifecycle.State.INITIALIZED) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
}
}
});
+10 -10
View File
@@ -1,23 +1,23 @@
apply from: rootProject.file('dependencies.gradle')
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.autodispose
api rootProject.ext.autodisposeLifecycle
api libs.rxjava2
api libs.autodispose
api libs.autodispose.lifecycle
implementation project(':conductor')
}
+11 -11
View File
@@ -1,26 +1,26 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
testImplementation libs.junit
testImplementation libs.robolectric
implementation rootProject.ext.androidxAppCompat
implementation libs.androidx.appcompat
implementation project(':conductor')
}
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor-viewpager'
apply plugin: "com.vanniktech.maven.publish"
@@ -77,7 +77,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
savedPages.remove(position);
}
Router router = host.getChildRouter(container, name);
Router router = host.getChildRouter(container, name)
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER);
if (!router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
+13 -13
View File
@@ -1,13 +1,16 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
@@ -20,15 +23,12 @@ android {
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
testImplementation libs.junit
testImplementation libs.robolectric
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxViewPager2
implementation libs.androidx.appcompat
implementation libs.androidx.viewpager2
implementation project(':conductor')
}
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor-viewpager2'
apply plugin: "com.vanniktech.maven.publish"
@@ -154,7 +154,8 @@ 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)!!
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
// This should have already been handled by onViewRecycled, but it seems like this wasn't
// always reliably called
+15 -15
View File
@@ -1,12 +1,15 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
consumerProguardFiles 'proguard-rules.txt'
@@ -14,20 +17,17 @@ android {
}
dependencies {
implementation savedState
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
testImplementation kotestAssertions
implementation libs.androidx.savedstate.ktx
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.kotest
implementation archComponentsLifecycle
implementation libs.androidx.lifecycle.runtime
api rootProject.ext.androidxAnnotations
api kotlinStd
api libs.androidx.annotation
api libs.kotlin.stdlib
lintPublish project(':conductor-lint')
}
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor'
apply plugin: "com.vanniktech.maven.publish"
@@ -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);
@@ -693,7 +709,7 @@ public abstract class Controller {
public void setRetainViewMode(@NonNull RetainViewMode retainViewMode) {
this.retainViewMode = retainViewMode != null ? retainViewMode : RetainViewMode.RELEASE_DETACH;
if (this.retainViewMode == RetainViewMode.RELEASE_DETACH && !attached) {
removeViewReference();
removeViewReference(null);
}
}
@@ -856,6 +872,27 @@ public abstract class Controller {
}
}
final void onContextUnavailable(@NonNull Context context) {
for (Router childRouter : childRouters) {
childRouter.onContextUnavailable(context);
}
if (isContextAvailable) {
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) {
@@ -971,7 +995,7 @@ public abstract class Controller {
}
}
void detach(@NonNull View view, boolean forceViewRefRemoval, boolean blockViewRefRemoval) {
void detach(View view, boolean forceViewRefRemoval, boolean blockViewRefRemoval) {
if (!attachedToUnownedParent) {
for (ControllerHostedRouter router : childRouters) {
router.prepareForHostDetach();
@@ -981,34 +1005,41 @@ 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();
removeViewReference(view != null ? view.getContext() : null);
}
}
private void removeViewReference() {
private void removeViewReference(@Nullable Context context) {
if (view != null) {
if (context == null) {
context = view.getContext();
}
if (!isBeingDestroyed && !hasSavedViewState) {
saveViewState(view);
}
@@ -1044,14 +1075,15 @@ public abstract class Controller {
}
if (isBeingDestroyed) {
performDestroy();
performDestroy(context);
}
}
final View inflate(@NonNull ViewGroup parent) {
if (view != null && view.getParent() != null && view.getParent() != parent) {
View viewRef = view;
detach(view, true, false);
removeViewReference();
removeViewReference(viewRef.getContext());
}
if (view == null) {
@@ -1121,20 +1153,13 @@ public abstract class Controller {
}
}
private void performDestroy() {
private void performDestroy(@Nullable Context context) {
if (context == null) {
context = getActivity();
}
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(context);
}
if (!destroyed) {
@@ -1172,7 +1197,7 @@ public abstract class Controller {
}
if (!attached) {
removeViewReference();
removeViewReference(null);
} else if (removeViews) {
detach(view, true, false);
}
@@ -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,12 +221,13 @@ 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);
RouterTransaction topTransaction = null;
if (popViews && poppedControllers.size() > 0) {
RouterTransaction topTransaction = poppedControllers.get(0);
topTransaction = poppedControllers.get(0);
topTransaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
@@ -240,6 +242,16 @@ public abstract class Router {
performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler());
}
if (poppedControllers.size() > 0) {
NoOpControllerChangeHandler changeHandler = new NoOpControllerChangeHandler();
for (RouterTransaction routerTransaction : poppedControllers) {
if (routerTransaction != topTransaction) {
routerTransaction.controller().changeStarted(changeHandler, ControllerChangeType.POP_EXIT);
routerTransaction.controller().changeEnded(changeHandler, ControllerChangeType.POP_EXIT);
}
}
}
}
public int getContainerId() {
@@ -250,10 +262,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;
}
@@ -545,10 +571,9 @@ public abstract class Router {
public void rebindIfNeeded() {
ThreadUtils.ensureMainThread();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
// Not directly using the iterator in order to prevent ConcurrentModificationExceptions if controllers pop
// themselves on re-attach.
for (RouterTransaction transaction : getTransactions()) {
if (transaction.controller().getNeedsAttach()) {
performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false));
} else {
@@ -649,14 +674,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 +752,7 @@ public abstract class Router {
@Override
public void run() {
containerFullyAttached = true;
performPendingControllerChanges();
}
});
}
@@ -745,6 +771,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());
@@ -757,6 +792,18 @@ public abstract class Router {
return controllers;
}
@NonNull
final List<RouterTransaction> getTransactions() {
List<RouterTransaction> transactions = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
transactions.add(backstackIterator.next());
}
return transactions;
}
@Nullable
public final Boolean handleRequestedPermission(@NonNull String permission) {
for (RouterTransaction transaction : backstack) {
@@ -793,7 +840,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 +884,9 @@ public abstract class Router {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
if (container != null) {
container.post(this::performPendingControllerChanges);
}
} else {
ControllerChangeHandler.executeChange(transaction);
}
@@ -1001,4 +1045,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
@@ -84,29 +85,19 @@ 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)
pauseOnChangeStart(
targetController = controller,
changeController = changeController,
changeHandler = changeHandler,
changeType = changeType,
)
savedRegistryState = Bundle()
savedStateRegistryController.performSave(savedRegistryState)
hasSavedState = true
}
GlobalChangeStartListener.onChangeStart(changeController, changeHandler, changeType)
}
override fun preDetach(controller: Controller, view: View) {
@@ -135,8 +126,7 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
override fun preDestroyView(controller: Controller, view: View) {
if (controller.isBeingDestroyed && controller.router.backstackSize == 0) {
val parent = view.parent as? View
parent?.addOnAttachStateChangeListener(object :
View.OnAttachStateChangeListener {
parent?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) = Unit
override fun onViewDetachedFromWindow(v: View?) {
parent.removeOnAttachStateChangeListener(this)
@@ -147,6 +137,14 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
override fun postContextAvailable(controller: Controller, context: Context) {
listenForAncestorChangeStart(controller)
}
override fun preContextUnavailable(controller: Controller, context: Context) {
stopListeningForAncestorChangeStart(controller)
}
})
}
@@ -154,11 +152,100 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
override fun getSavedStateRegistry() = savedStateRegistryController.savedStateRegistry
private fun listenForAncestorChangeStart(controller: Controller) {
GlobalChangeStartListener.subscribe(controller, controller.ancestors()) { ancestor, changeHandler, changeType ->
// No-op on the case where we (the child controller) hasn't yet created a View as our parent is being
// changed out.
if (::lifecycleRegistry.isInitialized) {
pauseOnChangeStart(
targetController = ancestor,
changeController = ancestor,
changeHandler = changeHandler,
changeType = changeType,
)
}
}
}
private fun stopListeningForAncestorChangeStart(controller: Controller) {
GlobalChangeStartListener.unsubscribe(controller)
}
// 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
}
}
private fun Controller.ancestors(): Collection<String> {
return buildList {
var ancestor = parentController
while (ancestor != null) {
add(ancestor.instanceId)
ancestor = ancestor.parentController
}
}
}
companion object {
private const val KEY_SAVED_STATE = "Registry.savedState"
fun own(target: Controller) {
OwnViewTreeLifecycleAndRegistry(target)
fun own(target: Controller): OwnViewTreeLifecycleAndRegistry {
return OwnViewTreeLifecycleAndRegistry(target)
}
}
}
// In order to prevent child controllers from having strong references to all of their ancestors, some of which may
// break their connection before the child is made aware, this shared listener is used to call all interested parties
// when a controller begins transitioning.
private object GlobalChangeStartListener {
private val listeners = mutableMapOf<String, Listener>()
fun subscribe(
controller: Controller,
targetControllers: Collection<String>,
listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
) {
listeners[controller.instanceId] = Listener(targetControllers, listener)
}
fun unsubscribe(controller: Controller) {
listeners.remove(controller.instanceId)
}
fun onChangeStart(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
listeners.values.forEach { it.call(controller, changeHandler, changeType) }
}
private class Listener(
private val targetControllers: Collection<String>,
private val listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
) {
fun call(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
if (targetControllers.contains(controller.instanceId)) {
listener(controller, changeHandler, changeType)
}
}
}
}
@@ -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(),
@@ -155,7 +155,7 @@ class ControllerLifecycleActivityReferenceTests {
)
activity.router.popCurrentController()
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(listOf(true), listener.postDetachReferences)
@@ -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(),
@@ -208,7 +208,7 @@ class ControllerLifecycleActivityReferenceTests {
)
activityController.pause().stop().destroy()
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(listOf(true), listener.postDetachReferences)
@@ -465,6 +465,8 @@ class ControllerLifecycleCallbacksTests {
assertCalls(expectedCallState, child)
activityController.get().router.popCurrentController()
expectedCallState.changeStartCalls++
expectedCallState.changeEndCalls++
expectedCallState.detachCalls++
expectedCallState.destroyViewCalls++
expectedCallState.contextUnavailableCalls++
@@ -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)
}
}
+15 -20
View File
@@ -5,7 +5,7 @@ android {
defaultConfig {
applicationId "com.bluelinelabs.conductor.demo"
minSdkVersion 21
targetSdkVersion rootProject.ext.targetSdkVersion
targetSdkVersion libs.versions.targetsdk.get()
versionCode 1
versionName "1.0.0"
vectorDrawables.useSupportLibrary true
@@ -27,7 +27,7 @@ android {
compose = true
}
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -39,21 +39,19 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion composeVersion
kotlinCompilerExtensionVersion libs.versions.compose.get()
}
}
dependencies {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxViewPager2
implementation rootProject.ext.material
implementation rootProject.ext.androidxCoreKtx
implementation libs.androidx.appcompat
implementation libs.androidx.viewpager2
implementation libs.material
implementation libs.androidx.core.ktx
implementation rootProject.ext.archComponentsLiveDataCore // Fix duplicate classes
implementation libs.picasso
implementation rootProject.ext.picasso
implementation rootProject.ext.autodisposeKtx
implementation libs.autodispose.ktx
implementation project(':conductor')
implementation project(':conductor-modules:viewpager')
@@ -62,14 +60,11 @@ dependencies {
implementation project(':conductor-modules:arch-components-lifecycle')
implementation project(':conductor-modules:androidx-transition')
implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
implementation "androidx.compose.foundation:foundation:$composeVersion"
implementation "androidx.compose.material:material:$composeVersion"
implementation "androidx.compose.material:material-icons-core:$composeVersion"
implementation "androidx.compose.material:material-icons-extended:$composeVersion"
implementation "androidx.activity:activity-compose:1.3.0-beta02"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"
implementation libs.compose.ui
implementation libs.compose.ui.tooling
implementation libs.compose.foundation
implementation libs.compose.material
implementation libs.activity.compose
implementation rootProject.ext.leakCanary
implementation libs.leakCanary
}
@@ -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,5 +1,6 @@
package com.bluelinelabs.conductor.demo.controllers.base
import android.view.View
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
@@ -15,6 +16,13 @@ private class RefWatchingControllerLifecycleListener : Controller.LifecycleListe
}
}
override fun preDestroyView(controller: Controller, view: View) {
AppWatcher.objectWatcher.expectWeaklyReachable(
view,
"A destroyed controller view should have only weak references."
)
}
override fun onChangeEnd(
controller: Controller,
changeHandler: ControllerChangeHandler,
-55
View File
@@ -1,55 +0,0 @@
ext {
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 28
picassoVersion = '2.5.2'
rxJava2Version = '2.1.14'
autodisposeVersion = '1.0.0'
archComponentsVersion = '2.3.1'
junitVersion = '4.12'
mvnPublishVersion = '0.13.0'
dokkaVersion = '1.4.32'
composeVersion = "1.0.0-beta09"
agpVersion = "7.0.0-beta05"
lintVersion = agpVersion.replaceFirst(~/\d*/) { version ->
// the major version of lint is always 23 version higher than the major version of agp
version.toInteger() + 23
}
kotlinVersion = '1.5.10'
kotlinStd = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
material = "com.google.android.material:material:1.1.0"
androidxAnnotations = "androidx.annotation:annotation:1.1.0"
androidxAppCompat = "androidx.appcompat:appcompat:1.3.0"
androidxTransition = "androidx.transition:transition:1.3.1"
androidxCollection = "androidx.collection:collection:1.1.0"
androidxViewPager2 = "androidx.viewpager2:viewpager2:1.0.0"
androidxCoreKtx = "androidx.core:core-ktx:1.3.2"
picasso = "com.squareup.picasso:picasso:$picassoVersion"
leakCanary = "com.squareup.leakcanary:leakcanary-android:2.7"
rxJava2 = "io.reactivex.rxjava2:rxjava:$rxJava2Version"
autodispose = "com.uber.autodispose:autodispose:$autodisposeVersion"
autodisposeLifecycle = "com.uber.autodispose:autodispose-lifecycle:$autodisposeVersion"
autodisposeKtx = "com.uber.autodispose:autodispose-ktx:$autodisposeVersion"
archComponentsLifecycle = "androidx.lifecycle:lifecycle-runtime:$archComponentsVersion"
archComponentsLiveDataCore = "androidx.lifecycle:lifecycle-livedata-core:$archComponentsVersion"
savedState = "androidx.savedstate:savedstate-ktx:1.1.0"
junit = "junit:junit:$junitVersion"
robolectric = "org.robolectric:robolectric:4.5.1"
kotestAssertions = "io.kotest:kotest-assertions-core:4.6.0"
lintapi = "com.android.tools.lint:lint-api:$lintVersion"
lintchecks = "com.android.tools.lint:lint-checks:$lintVersion"
lint = "com.android.tools.lint:lint:$lintVersion"
lintTests = "com.android.tools.lint:lint-tests:$lintVersion"
}
+3 -1
View File
@@ -1,5 +1,5 @@
VERSION_CODE=3
VERSION_NAME=3.1.2-SNAPSHOT
VERSION_NAME=3.2.1-SNAPSHOT
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
@@ -14,3 +14,5 @@ POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=erickuck
POM_DEVELOPER_NAME=Eric Kuck
SONATYPE_HOST=DEFAULT
RELEASE_SIGNING_ENABLED=true
+66
View File
@@ -0,0 +1,66 @@
[versions]
minsdk = "16"
compilesdk = "28"
targetsdk = "28"
activity-compose = "1.3.0"
agp = "7.2.1" # Note: update lint version whenever this is updated
androidx-annotation = "1.1.0"
androidx-appcompat = "1.3.0"
androidx-collection = "1.1.0"
androidx-core = "1.3.2"
androidx-lifecycle = "2.3.1"
androidx-savedstate = "1.1.0"
androidx-transition = "1.3.1"
androidx-viewpager2 = "1.0.0"
autodispose = "1.0.0"
compose = "1.1.0"
dokka = "1.4.32"
junit = "4.13"
kotest = "4.6.0"
kotlin = "1.6.10"
leakCanary = "2.7"
lint = "30.2.1" # Should always be agp + 23
material = "1.2.1"
mvnpublish = "0.23.2"
picasso = "2.5.2"
robolectric = "4.5.1"
rxjava2 = "2.1.14"
[libraries]
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" }
androidx-lifecycle-livedata-core = { module = "androidx.lifecycle:lifecycle-livedata-core", version.ref = "androidx-lifecycle" }
androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "androidx-savedstate" }
androidx-transition = { module = "androidx.transition:transition", version.ref = "androidx-transition" }
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" }
autodispose = { module = "com.uber.autodispose:autodispose", version.ref = "autodispose" }
autodispose-lifecycle = { module = "com.uber.autodispose:autodispose-lifecycle", version.ref = "autodispose" }
autodispose-ktx = { module = "com.uber.autodispose:autodispose-ktx", version.ref = "autodispose" }
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
junit = { module = "junit:junit", version.ref = "junit" }
kotest = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
leakCanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakCanary" }
lint = { module = "com.android.tools.lint:lint", version.ref = "lint" }
lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" }
lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "lint" }
lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "lint" }
material = { module = "com.google.android.material:material", version.ref = "material" }
picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
rxjava2 = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava2" }
[plugins]
mvnpublish = { id = "com.vanniktech.maven.publish", version.ref = "mvnpublish" }
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists