Compare commits

...

7 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
17 changed files with 138 additions and 71 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 }}
+2 -2
View File
@@ -20,7 +20,7 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
def conductorVersion = '3.1.8'
def conductorVersion = '3.2.0'
implementation "com.bluelinelabs:conductor:$conductorVersion"
@@ -42,7 +42,7 @@ implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
**SNAPSHOT**
Just use `3.1.9-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 {
+16 -1
View File
@@ -6,7 +6,6 @@ buildscript {
dependencies {
classpath libs.agp
classpath libs.kotlin.plugin
classpath libs.mvnpublish
classpath libs.dokka
}
}
@@ -16,4 +15,20 @@ allprojects {
mavenCentral()
google()
}
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
}
}
}
}
}
}
}
@@ -1,4 +1,7 @@
apply plugin: 'com.android.library'
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
@@ -19,5 +22,3 @@ dependencies {
}
ext.artifactId = 'conductor-androidx-transition'
apply plugin: "com.vanniktech.maven.publish"
@@ -1,4 +1,7 @@
apply plugin: 'com.android.library'
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
@@ -18,5 +21,3 @@ dependencies {
}
ext.artifactId = 'conductor-arch-components-lifecycle'
apply plugin: "com.vanniktech.maven.publish"
+4 -2
View File
@@ -1,5 +1,7 @@
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
+5 -3
View File
@@ -1,5 +1,8 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
@@ -21,4 +24,3 @@ dependencies {
}
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);
+6 -4
View File
@@ -1,6 +1,9 @@
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 libs.versions.compilesdk.get() as Integer
@@ -29,4 +32,3 @@ dependencies {
}
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
+5 -3
View File
@@ -1,5 +1,8 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
@@ -28,4 +31,3 @@ dependencies {
}
ext.artifactId = 'conductor'
apply plugin: "com.vanniktech.maven.publish"
@@ -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() {
@@ -874,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) {
@@ -138,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)
}
})
}
@@ -151,40 +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
) {
// 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 = 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
@@ -213,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"
@@ -221,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++
+3 -1
View File
@@ -1,5 +1,5 @@
VERSION_CODE=3
VERSION_NAME=3.1.9-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
+4 -2
View File
@@ -22,7 +22,7 @@ kotlin = "1.6.10"
leakCanary = "2.7"
lint = "30.2.1" # Should always be agp + 23
material = "1.2.1"
mvnpublish = "0.13.0"
mvnpublish = "0.23.2"
picasso = "2.5.2"
robolectric = "4.5.1"
rxjava2 = "2.1.14"
@@ -58,7 +58,9 @@ 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" }
mvnpublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mvnpublish" }
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" }