Compare commits

...

21 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
25 changed files with 326 additions and 233 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
+10 -8
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.5'
def conductorVersion = '3.2.0'
implementation "com.bluelinelabs:conductor:$conductorVersion"
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.5'
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.1.5'
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
// ViewPager2 Adapter:
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.5'
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.1.5'
implementation "com.bluelinelabs:conductor-autodispose:$conductorVersion"
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.5'
implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
```
**SNAPSHOT**
Just use `3.1.6-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 {
+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"
@@ -155,6 +155,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", 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"
@@ -709,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);
}
}
@@ -873,11 +873,11 @@ public abstract class Controller {
}
final void onContextUnavailable(@NonNull Context context) {
if (isContextAvailable) {
for (Router childRouter : childRouters) {
childRouter.onContextUnavailable(context);
}
for (Router childRouter : childRouters) {
childRouter.onContextUnavailable(context);
}
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, context);
@@ -995,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();
@@ -1030,12 +1030,16 @@ public abstract class Controller {
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);
}
@@ -1071,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) {
@@ -1148,9 +1153,13 @@ public abstract class Controller {
}
}
private void performDestroy() {
private void performDestroy(@Nullable Context context) {
if (context == null) {
context = getActivity();
}
if (isContextAvailable) {
onContextUnavailable(getActivity());
onContextUnavailable(context);
}
if (!destroyed) {
@@ -1188,7 +1197,7 @@ public abstract class Controller {
}
if (!attached) {
removeViewReference();
removeViewReference(null);
} else if (removeViews) {
detach(view, true, false);
}
@@ -225,8 +225,9 @@ public abstract class Router {
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) {
@@ -241,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() {
@@ -560,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 {
@@ -782,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) {
@@ -863,12 +885,7 @@ public abstract class Router {
}
pendingControllerChanges.add(transaction);
if (container != null) {
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
container.post(this::performPendingControllerChanges);
}
} else {
ControllerChangeHandler.executeChange(transaction);
@@ -29,7 +29,6 @@ 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() {
@@ -97,6 +96,8 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
changeHandler = changeHandler,
changeType = changeType,
)
GlobalChangeStartListener.onChangeStart(changeController, changeHandler, changeType)
}
override fun preDetach(controller: Controller, view: View) {
@@ -125,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)
@@ -139,11 +139,11 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
}
override fun postContextAvailable(controller: Controller, context: Context) {
listenForParentChangeStart(controller)
listenForAncestorChangeStart(controller)
}
override fun preContextUnavailable(controller: Controller, context: Context) {
stopListeningForParentChangeStart(controller)
stopListeningForAncestorChangeStart(controller)
}
})
}
@@ -152,36 +152,23 @@ 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,
)
}
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,
)
}
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)
}
}
private fun stopListeningForAncestorChangeStart(controller: Controller) {
GlobalChangeStartListener.unsubscribe(controller)
}
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
@@ -210,6 +197,16 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
}
}
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"
@@ -218,3 +215,37 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
}
}
}
// 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)
}
}
}
}
@@ -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)
@@ -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++
+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
}
@@ -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.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
}
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.6-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