Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3f7d128f5 | |||
| 5e1f072672 | |||
| b0d15d9f9e | |||
| 8ac2e04c62 | |||
| cdbdee5c42 | |||
| 8488242a26 | |||
| 1fe0187439 | |||
| f78726b916 | |||
| 1f918f10c5 | |||
| bd584727be | |||
| 91db7fe65f | |||
| 2abe2b33f9 | |||
| ac4e09cf67 | |||
| 055532bb21 |
@@ -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 }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
 [](http://android-arsenal.com/details/1/3361) [](http://javadoc.io/doc/com.bluelinelabs/conductor)
|
||||
[](https://github.com/bluelinelabs/conductor/actions/workflows/main.yml) [](http://android-arsenal.com/details/1/3361) [](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.6'
|
||||
def conductorVersion = '3.2.0'
|
||||
|
||||
implementation "com.bluelinelabs:conductor:$conductorVersion"
|
||||
|
||||
// AndroidX Transition change handlers:
|
||||
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.6'
|
||||
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
|
||||
|
||||
// ViewPager PagerAdapter:
|
||||
implementation 'com.bluelinelabs:conductor-viewpager:3.1.6'
|
||||
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
|
||||
|
||||
// ViewPager2 Adapter:
|
||||
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.6'
|
||||
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
|
||||
|
||||
// RxJava2 Autodispose support:
|
||||
implementation 'com.bluelinelabs:conductor-autodispose:3.1.6'
|
||||
implementation "com.bluelinelabs:conductor-autodispose:$conductorVersion"
|
||||
|
||||
// Lifecycle-aware Controllers (architecture components):
|
||||
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.6'
|
||||
implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
|
||||
```
|
||||
|
||||
**SNAPSHOT**
|
||||
|
||||
Just use `3.1.7-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
@@ -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
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1081,8 +1081,9 @@ public abstract class Controller {
|
||||
|
||||
final View inflate(@NonNull ViewGroup parent) {
|
||||
if (view != null && view.getParent() != null && view.getParent() != parent) {
|
||||
View viewRef = view;
|
||||
detach(view, true, false);
|
||||
removeViewReference(view.getContext());
|
||||
removeViewReference(viewRef.getContext());
|
||||
}
|
||||
|
||||
if (view == null) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+61
-33
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
@@ -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
@@ -1,5 +1,5 @@
|
||||
VERSION_CODE=3
|
||||
VERSION_NAME=3.1.6
|
||||
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
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user