Compare commits

...

48 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
EricKuck 211da8b2ea Version bump for 3.1.1 release 2021-07-19 17:42:43 -05:00
EricKuck 26db962168 Revert maven-publish plugin migration 2021-07-19 17:41:17 -05:00
EricKuck f4c1c6ccf5 Provide a bit more safety around pushing controllers outside of normal lifecycle events 2021-07-19 17:20:32 -05:00
Mario c89caa87e0 Apply the kotlin plugin to lint module (#653)
Fixes missing IssueRegistry from lint.jar
2021-07-19 13:08:34 -05:00
Paul Woitaschek 2748566437 Make the StringSparseArrayParceler internal. 2021-07-03 22:09:39 +02:00
Paul Woitaschek 506c99ed41 Convert the StringSparseArrayParceler to kotlin. 2021-07-03 22:08:18 +02:00
Paul Woitaschek 4fe0ec5f51 Rename .java to .kt 2021-07-03 22:08:18 +02:00
Paul Woitaschek afa93f2cc1 Unit-test the StringSparseArrayParceler. 2021-07-03 22:02:08 +02:00
Paul Woitaschek 836f92b615 Update the publish task name. 2021-07-03 21:41:21 +02:00
Paul Woitaschek 94c817bbd9 Remove support for rxLifecycle2. 2021-07-03 21:40:43 +02:00
Paul Woitaschek fc1fee3e17 Kotlin backstack (#652)
Migrate the Backstack to kotlin
2021-07-03 21:32:11 +02:00
Paul Woitaschek 76b7572a01 Migrate to the maven-publish plugin. (#651) 2021-07-03 21:31:24 +02:00
Paul Woitaschek 3fc63b7f5f Remove the deprecated kotlin-android-extensions plugin and use the kotlin-parcelize plugin replacement instead. 2021-07-03 14:37:40 +02:00
Paul Woitaschek 0ef52211a2 Update LeakCanary to 2.7. (#650)
Also simplify the usage by extracting the leak watching logic into a lifecycle listener.
2021-07-03 14:31:04 +02:00
Paul Woitaschek 7574131940 Remove the unused rxJavaVersion declaration. 2021-07-02 22:40:01 +02:00
Paul Woitaschek 1ab9a4c4f6 Remove the unused rxLifecycleVersion declaration. 2021-07-02 22:35:52 +02:00
Paul Woitaschek 3bc23bd5cd Update to agp 7.0.0-beta05 2021-07-02 22:31:53 +02:00
57 changed files with 1029 additions and 1007 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 -12
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,30 +20,29 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
implementation 'com.bluelinelabs:conductor:3.1.0'
def conductorVersion = '3.2.0'
implementation "com.bluelinelabs:conductor:$conductorVersion"
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.0'
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.1.0'
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
// ViewPager2 Adapter:
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.0'
// RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.1.0'
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.1.0'
implementation "com.bluelinelabs:conductor-autodispose:$conductorVersion"
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.0'
implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
```
**SNAPSHOT**
Just use `3.1.1-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 {
@@ -79,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
}
}
}
}
}
}
}
+8 -9
View File
@@ -1,16 +1,17 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
configurations {
lintChecks
libs.lint.checks
}
dependencies {
compileOnly rootProject.ext.lintapi
compileOnly rootProject.ext.lintchecks
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 {
@@ -18,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')
}
@@ -1,25 +0,0 @@
apply from: rootProject.file('dependencies.gradle')
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.rxLifecycle2
api rootProject.ext.rxLifecycleAndroid2
implementation project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle2'
@@ -1,3 +0,0 @@
POM_NAME=Conductor RxLifecycle2 Extensions
POM_ARTIFACT_ID=conductor-rxlifecycle2
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle2">
<application />
</manifest>
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,71 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() {
}
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -1,49 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController(){
this(null);
}
public RxController(@Nullable Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -1,43 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import androidx.annotation.NonNull;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle2.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link io.reactivex.ObservableTransformer} that unsubscribes the source during the Controller lifecycle
*/
public static <T> LifecycleTransformer<T> bindController(@NonNull final Observable<ControllerEvent> lifecycle) {
return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE);
}
private static final Function<ControllerEvent, ControllerEvent> CONTROLLER_LIFECYCLE =
new Function<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
}
+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')
}
ext.artifactId = 'conductor-viewpager'
apply from: rootProject.file('dependencies.gradle')
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-android-extensions'
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')
}
ext.artifactId = 'conductor-viewpager2'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -10,7 +10,7 @@ import androidx.viewpager2.adapter.StatefulAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
/**
* An ViewPager2 adapter that uses Routers as pages
@@ -48,7 +48,8 @@ abstract class RouterStateAdapter(private val host: Controller) :
}
private fun inferViewPager(recyclerView: RecyclerView): ViewPager2 {
return recyclerView.parent as? ViewPager2 ?: error("Expected ViewPager2 instance. Got: ${recyclerView.parent}")
return recyclerView.parent as? ViewPager2
?: error("Expected ViewPager2 instance. Got: ${recyclerView.parent}")
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
@@ -119,7 +120,8 @@ abstract class RouterStateAdapter(private val host: Controller) :
override fun saveState(): Parcelable {
// Ensure all visible pages are saved, starting at the outermost pages and working our way in
val visiblePositions = (0 until visibleRouters.size()).map { visibleRouters.keyAt(it) }.toMutableList()
val visiblePositions = (0 until visibleRouters.size())
.map { visibleRouters.keyAt(it) }.toMutableList()
while (visiblePositions.isNotEmpty()) {
val lastPosition = visiblePositions.removeAt(visiblePositions.lastIndex)
savePage(getItemId(lastPosition), visibleRouters[lastPosition])
@@ -152,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 -14
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,19 +17,17 @@ android {
}
dependencies {
implementation savedState
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
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')
}
ext.artifactId = 'conductor'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -1,118 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
class Backstack implements Iterable<RouterTransaction> {
private static final String KEY_ENTRIES = "Backstack.entries";
private final Deque<RouterTransaction> backstack = new ArrayDeque<>();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isEmpty() {
return backstack.isEmpty();
}
int size() {
return backstack.size();
}
@Nullable
RouterTransaction root() {
return backstack.size() > 0 ? backstack.getLast() : null;
}
@Override @NonNull
public Iterator<RouterTransaction> iterator() {
return backstack.iterator();
}
@NonNull
Iterator<RouterTransaction> reverseIterator() {
return backstack.descendingIterator();
}
@NonNull
List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popped = new ArrayList<>();
if (backstack.contains(transaction)) {
while (backstack.peek() != transaction) {
RouterTransaction poppedTransaction = pop();
popped.add(poppedTransaction);
}
} else {
throw new RuntimeException("Tried to pop to a transaction that was not on the back stack");
}
return popped;
}
@NonNull
RouterTransaction pop() {
RouterTransaction popped = backstack.pop();
popped.controller().destroy();
return popped;
}
@Nullable
RouterTransaction peek() {
return backstack.peek();
}
void push(@NonNull RouterTransaction transaction) {
backstack.push(transaction);
}
@NonNull
List<RouterTransaction> popAll() {
List<RouterTransaction> list = new ArrayList<>();
while (!isEmpty()) {
list.add(pop());
}
return list;
}
void setBackstack(@NonNull List<RouterTransaction> backstack) {
this.backstack.clear();
for (RouterTransaction transaction : backstack) {
this.backstack.push(transaction);
}
}
boolean contains(@NonNull Controller controller) {
for (RouterTransaction transaction : backstack) {
if (controller == transaction.controller()) {
return true;
}
}
return false;
}
void saveInstanceState(@NonNull Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backstack.size());
for (RouterTransaction entry : backstack) {
entryBundles.add(entry.saveInstanceState());
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
}
void restoreInstanceState(@NonNull Bundle savedInstanceState) {
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
if (entryBundles != null) {
Collections.reverse(entryBundles);
for (Bundle transactionBundle : entryBundles) {
backstack.push(new RouterTransaction(transactionBundle));
}
}
}
}
@@ -0,0 +1,89 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import java.util.*
internal class Backstack : Iterable<RouterTransaction> {
private val backstack: Deque<RouterTransaction> = ArrayDeque()
val isEmpty: Boolean get() = backstack.isEmpty()
val size: Int get() = backstack.size
fun root(): RouterTransaction? = backstack.lastOrNull()
override fun iterator(): MutableIterator<RouterTransaction> {
return backstack.iterator()
}
fun reverseIterator(): Iterator<RouterTransaction> = backstack.descendingIterator()
fun popTo(transaction: RouterTransaction): List<RouterTransaction> {
if (transaction in backstack) {
val popped: MutableList<RouterTransaction> = ArrayList()
while (backstack.peek() != transaction) {
val poppedTransaction = pop()
popped.add(poppedTransaction)
}
return popped
} else {
throw RuntimeException("Tried to pop to a transaction that was not on the back stack")
}
}
fun pop(): RouterTransaction {
return backstack.pop().also {
it.controller.destroy()
}
}
fun peek(): RouterTransaction? = backstack.peek()
fun push(transaction: RouterTransaction) {
backstack.push(transaction)
}
fun popAll(): List<RouterTransaction> {
val list: MutableList<RouterTransaction> = ArrayList()
while (!isEmpty) {
list.add(pop())
}
return list
}
fun setBackstack(backstack: List<RouterTransaction>) {
this.backstack.clear()
backstack.forEach { transaction ->
this.backstack.push(transaction)
}
}
operator fun contains(controller: Controller): Boolean {
return backstack.any {
it.controller == controller
}
}
fun saveInstanceState(outState: Bundle) {
val entryBundles = ArrayList<Bundle>(backstack.size)
backstack.mapTo(entryBundles) {
it.saveInstanceState()
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles)
}
fun restoreInstanceState(savedInstanceState: Bundle) {
val entryBundles = savedInstanceState.getParcelableArrayList<Bundle?>(KEY_ENTRIES)
if (entryBundles != null) {
entryBundles.reverse()
for (transactionBundle in entryBundles) {
backstack.push(RouterTransaction(transactionBundle!!))
}
}
}
companion object {
private const val KEY_ENTRIES = "Backstack.entries"
}
}
@@ -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;
}
@@ -280,7 +306,7 @@ public abstract class Router {
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
if (backstack.size() > 1) {
if (backstack.getSize() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
return true;
@@ -375,7 +401,7 @@ public abstract class Router {
*/
@SuppressWarnings("WeakerAccess")
public int getBackstackSize() {
return backstack.size();
return backstack.getSize();
}
/**
@@ -383,7 +409,7 @@ public abstract class Router {
*/
@NonNull
public List<RouterTransaction> getBackstack() {
List<RouterTransaction> list = new ArrayList<>(backstack.size());
List<RouterTransaction> list = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
list.add(backstackIterator.next());
@@ -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()) {
@@ -700,7 +725,7 @@ public abstract class Router {
}
private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
if (backstack.size() > 0) {
if (backstack.getSize() > 0) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> updatedBackstack = new ArrayList<>();
@@ -727,6 +752,7 @@ public abstract class Router {
@Override
public void run() {
containerFullyAttached = true;
performPendingControllerChanges();
}
});
}
@@ -745,9 +771,18 @@ 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.size());
List<Controller> controllers = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
@@ -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.size() == 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
@@ -74,7 +75,7 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
) {
// Should only happen if pushing another controller over this one was aborted
if (
controller == changeController &&
controller === changeController &&
changeType.isEnter &&
changeHandler.removesFromViewOnPush() &&
changeController.view?.windowToken != null &&
@@ -84,28 +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() &&
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) {
@@ -134,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)
@@ -146,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)
}
})
}
@@ -153,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)
}
}
}
}
@@ -1,62 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import android.util.SparseArray;
public class StringSparseArrayParceler implements Parcelable {
private final SparseArray<String> stringSparseArray;
public StringSparseArrayParceler(@NonNull SparseArray<String> stringSparseArray) {
this.stringSparseArray = stringSparseArray;
}
StringSparseArrayParceler(@NonNull Parcel in) {
stringSparseArray = new SparseArray<>();
final int size = in.readInt();
for (int i = 0; i < size; i++) {
stringSparseArray.put(in.readInt(), in.readString());
}
}
@NonNull
public SparseArray<String> getStringSparseArray() {
return stringSparseArray;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
final int size = stringSparseArray.size();
out.writeInt(size);
for (int i = 0; i < size; i++) {
int key = stringSparseArray.keyAt(i);
out.writeInt(key);
out.writeString(stringSparseArray.get(key));
}
}
public static final Parcelable.Creator<StringSparseArrayParceler> CREATOR = new Parcelable.Creator<StringSparseArrayParceler>() {
@Override
public StringSparseArrayParceler createFromParcel(Parcel in) {
return new StringSparseArrayParceler(in);
}
@Override
public StringSparseArrayParceler[] newArray(int size) {
return new StringSparseArrayParceler[size];
}
};
}
@@ -0,0 +1,39 @@
package com.bluelinelabs.conductor.internal
import android.os.Parcel
import android.os.Parcelable
import android.util.SparseArray
internal class StringSparseArrayParceler(val stringSparseArray: SparseArray<String>) : Parcelable {
override fun describeContents(): Int = 0
override fun writeToParcel(out: Parcel, flags: Int) {
val size = stringSparseArray.size()
out.writeInt(size)
for (i in 0 until size) {
val key = stringSparseArray.keyAt(i)
out.writeInt(key)
out.writeString(stringSparseArray[key])
}
}
companion object {
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<StringSparseArrayParceler> =
object : Parcelable.Creator<StringSparseArrayParceler> {
override fun createFromParcel(parcel: Parcel): StringSparseArrayParceler {
val stringSparseArray = SparseArray<String>()
val size = parcel.readInt()
for (i in 0 until size) {
stringSparseArray.put(parcel.readInt(), parcel.readString())
}
return StringSparseArrayParceler(stringSparseArray)
}
override fun newArray(size: Int): Array<StringSparseArrayParceler?> = arrayOfNulls(size)
}
}
}
@@ -1,67 +0,0 @@
package com.bluelinelabs.conductor;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BackstackTests {
private Backstack backstack;
@Before
public void setup() {
backstack = new Backstack();
}
@Test
public void testPush() {
assertEquals(0, backstack.size());
backstack.push(RouterTransaction.with(new TestController()));
assertEquals(1, backstack.size());
}
@Test
public void testPop() {
backstack.push(RouterTransaction.with(new TestController()));
backstack.push(RouterTransaction.with(new TestController()));
assertEquals(2, backstack.size());
backstack.pop();
assertEquals(1, backstack.size());
backstack.pop();
assertEquals(0, backstack.size());
}
@Test
public void testPeek() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
backstack.push(transaction1);
assertEquals(transaction1, backstack.peek());
backstack.push(transaction2);
assertEquals(transaction2, backstack.peek());
backstack.pop();
assertEquals(transaction1, backstack.peek());
}
@Test
public void testPopTo() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
RouterTransaction transaction3 = RouterTransaction.with(new TestController());
backstack.push(transaction1);
backstack.push(transaction2);
backstack.push(transaction3);
assertEquals(3, backstack.size());
backstack.popTo(transaction1);
assertEquals(1, backstack.size());
assertEquals(transaction1, backstack.peek());
}
}
@@ -0,0 +1,60 @@
package com.bluelinelabs.conductor
import org.junit.Assert.assertEquals
import org.junit.Test
class BackstackTests {
private val backstack = Backstack()
@Test
fun testPush() {
assertEquals(0, backstack.size.toLong())
backstack.push(TestController().asTransaction())
assertEquals(1, backstack.size.toLong())
}
@Test
fun testPop() {
backstack.push(TestController().asTransaction())
backstack.push(TestController().asTransaction())
assertEquals(2, backstack.size.toLong())
backstack.pop()
assertEquals(1, backstack.size.toLong())
backstack.pop()
assertEquals(0, backstack.size.toLong())
}
@Test
fun testPeek() {
val transaction1 = TestController().asTransaction()
val transaction2 = TestController().asTransaction()
backstack.push(transaction1)
assertEquals(transaction1, backstack.peek())
backstack.push(transaction2)
assertEquals(transaction2, backstack.peek())
backstack.pop()
assertEquals(transaction1, backstack.peek())
}
@Test
fun testPopTo() {
val transaction1 = TestController().asTransaction()
val transaction2 = TestController().asTransaction()
val transaction3 = TestController().asTransaction()
backstack.push(transaction1)
backstack.push(transaction2)
backstack.push(transaction3)
assertEquals(3, backstack.size.toLong())
backstack.popTo(transaction1)
assertEquals(1, backstack.size.toLong())
assertEquals(transaction1, backstack.peek())
}
}
@@ -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)
}
}
@@ -0,0 +1,50 @@
package com.bluelinelabs.conductor.internal
import android.os.Parcel
import android.os.Parcelable
import android.util.SparseArray
import io.kotest.matchers.ints.shouldBeExactly
import io.kotest.matchers.shouldBe
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StringSparseArrayParcelerTest {
@Test
fun emptyArray() {
SparseArray<String>().parcelAndUnParcel().size() shouldBeExactly 0
}
@Test
fun arrayWithContents() {
val array = SparseArray<String>()
array.put(1, "one")
array.put(7, "seven")
val unParceled = array.parcelAndUnParcel()
unParceled.size() shouldBeExactly 2
unParceled[1] shouldBe "one"
unParceled[7] shouldBe "seven"
}
private fun SparseArray<String>.parcelAndUnParcel(): SparseArray<String> {
val parceler = StringSparseArrayParceler(this)
val parcel = Parcel.obtain()
parceler.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
@Suppress("UNCHECKED_CAST")
val creator = StringSparseArrayParceler::class.java.getField("CREATOR").get(null)
as Parcelable.Creator<StringSparseArrayParceler>
val unParceled = creator.createFromParcel(parcel)
return unParceled.stringSparseArray.also {
check(it !== this)
}
}
}
+15 -23
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,40 +39,32 @@ 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')
implementation project(':conductor-modules:viewpager2')
implementation project(':conductor-modules:rxlifecycle2')
implementation project(':conductor-modules:autodispose')
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
debugImplementation rootProject.ext.leakCanary
releaseImplementation rootProject.ext.leakCanaryNoOp
testImplementation rootProject.ext.leakCanaryNoOp
implementation libs.leakCanary
}
+8 -9
View File
@@ -1,29 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.bluelinelabs.conductor.demo"
xmlns:android="http://schemas.android.com/apk/res/android">
package="com.bluelinelabs.conductor.demo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name="com.bluelinelabs.conductor.demo.DemoApplication"
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:fullBackupContent="true"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name="com.bluelinelabs.conductor.demo.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@@ -1,16 +0,0 @@
package com.bluelinelabs.conductor.demo
import android.app.Application
import com.squareup.leakcanary.LeakCanary
import com.squareup.leakcanary.RefWatcher
class DemoApplication : Application() {
override fun onCreate() {
super.onCreate()
refWatcher = LeakCanary.install(this)
}
companion object {
lateinit var refWatcher: RefWatcher
}
}
@@ -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()))
}
@@ -11,22 +11,20 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.archlifecycle.LifecycleController
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.DemoApplication
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.ToolbarProvider
import com.bluelinelabs.conductor.demo.controllers.base.watchForLeaks
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
class ArchLifecycleController : LifecycleController() {
private var hasExited = false
init {
Log.i(TAG, "Conductor: Constructor called")
watchForLeaks()
lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
@@ -104,21 +102,6 @@ class ArchLifecycleController : LifecycleController() {
override fun onDestroy() {
Log.i(TAG, "Conductor: onDestroy() called")
super.onDestroy()
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
companion object {
@@ -6,14 +6,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.DemoApplication
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.ToolbarProvider
import com.bluelinelabs.conductor.demo.controllers.base.watchForLeaks
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
import com.uber.autodispose.autoDisposable
import io.reactivex.Observable
@@ -23,10 +21,10 @@ import java.util.concurrent.TimeUnit
// instead of Activities or Fragments.
class AutodisposeController : Controller() {
private var hasExited = false
private val scopeProvider = ControllerScopeProvider.from(this)
init {
watchForLeaks()
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from constructor") }
.autoDisposable(scopeProvider)
@@ -98,20 +96,6 @@ class AutodisposeController : Controller() {
public override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy() called")
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
companion object {
@@ -2,10 +2,10 @@ package com.bluelinelabs.conductor.demo.controllers
import android.graphics.PorterDuff
import android.os.Bundle
import android.support.annotation.DrawableRes
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager
@@ -17,7 +17,9 @@ import com.bluelinelabs.conductor.demo.databinding.RowHomeBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class ExternalModulesController : BaseController(R.layout.controller_additional_modules) {
private val binding: ControllerAdditionalModulesBinding by viewBinding(ControllerAdditionalModulesBinding::bind)
private val binding: ControllerAdditionalModulesBinding by viewBinding(
ControllerAdditionalModulesBinding::bind
)
override val title = "External Module Demos"
@@ -40,11 +42,6 @@ class ExternalModulesController : BaseController(R.layout.controller_additional_
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
ModuleModel.RX_LIFECYCLE_2 -> router.pushController(
with(RxLifecycle2Controller())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
ModuleModel.ARCH_LIFECYCLE -> router.pushController(
with(ArchLifecycleController())
.pushChangeHandler(FadeChangeHandler())
@@ -56,7 +53,6 @@ class ExternalModulesController : BaseController(R.layout.controller_additional_
private enum class ModuleModel(val title: String, @ColorRes val color: Int) {
AUTODISPOSE("Autodispose", R.color.purple_300),
RX_LIFECYCLE_2("Rx Lifecycle 2", R.color.blue_grey_300),
ARCH_LIFECYCLE("Arch Components Lifecycle", R.color.orange_300);
}
@@ -83,7 +79,10 @@ private class AdditionalModulesAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ModuleModel) {
binding.title.text = item.title
binding.dot.drawable.setColorFilter(ContextCompat.getColor(itemView.context, item.color), PorterDuff.Mode.SRC_ATOP)
binding.dot.drawable.setColorFilter(
ContextCompat.getColor(itemView.context, item.color),
PorterDuff.Mode.SRC_ATOP
)
itemView.setOnClickListener { modelClickListener(item) }
}
}
@@ -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,119 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.DemoApplication
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.ToolbarProvider
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
import com.bluelinelabs.conductor.rxlifecycle2.ControllerEvent
import com.bluelinelabs.conductor.rxlifecycle2.RxController
import io.reactivex.Observable
import java.util.concurrent.TimeUnit
// Shamelessly borrowed from the official RxLifecycle demo by Trello and adapted for Conductor Controllers
// instead of Activities or Fragments.
class RxLifecycle2Controller : RxController() {
private var hasExited = false
init {
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from constructor") }
.compose(bindUntilEvent(ControllerEvent.DESTROY))
.subscribe { num: Long ->
Log.i(TAG, "Started in constructor, running until onDestroy(): $num")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
Log.i(TAG, "onCreateView() called")
val binding = ControllerLifecycleBinding.inflate(inflater, container, false)
binding.title.text = binding.root.resources.getString(R.string.rxlifecycle_title, TAG)
binding.nextReleaseView.setOnClickListener {
retainViewMode = RetainViewMode.RELEASE_DETACH
router.pushController(
with(TextController("Logcat should now report that the observables from onAttach() and onViewBound() have been disposed of, while the constructor observable is still running."))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
binding.nextRetainView.setOnClickListener {
retainViewMode = RetainViewMode.RETAIN_DETACH
router.pushController(
with(TextController("Logcat should now report that the observables from onAttach() has been disposed of, while the constructor and onViewBound() observables are still running."))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from onCreateView()") }
.compose(bindUntilEvent(ControllerEvent.DESTROY_VIEW))
.subscribe { num: Long ->
Log.i(TAG, "Started in onCreateView(), running until onDestroyView(): $num")
}
return binding.root
}
override fun onAttach(view: View) {
super.onAttach(view)
Log.i(TAG, "onAttach() called")
(activity as ToolbarProvider).toolbar.title = "RxLifecycle2 Demo"
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from onAttach()") }
.compose(bindUntilEvent(ControllerEvent.DETACH))
.subscribe { num: Long ->
Log.i(TAG, "Started in onAttach(), running until onDetach(): $num")
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
Log.i(TAG, "onDestroyView() called")
}
override fun onDetach(view: View) {
super.onDetach(view)
Log.i(TAG, "onDetach() called")
}
public override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy() called")
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
companion object {
private const val TAG = "RxLifecycleController"
}
}
@@ -14,7 +14,11 @@ import com.bluelinelabs.conductor.demo.ToolbarProvider
abstract class BaseController(
@LayoutRes private val layoutRes: Int,
args: Bundle? = null
) : RefWatchingController(args) {
) : Controller(args) {
init {
watchForLeaks()
}
// Note: This is just a quick demo of how an ActionBar *can* be accessed, not necessarily how it *should*
// be accessed. In a production app, this would use Dagger instead.
@@ -1,32 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers.base
import android.os.Bundle
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.demo.DemoApplication
abstract class RefWatchingController(args: Bundle?) : Controller(args) {
private var hasExited = false
public override fun onDestroy() {
super.onDestroy()
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
}
@@ -0,0 +1,47 @@
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
import leakcanary.AppWatcher
private class RefWatchingControllerLifecycleListener : Controller.LifecycleListener() {
private var hasExited = false
override fun postDestroy(controller: Controller) {
if (hasExited) {
controller.expectWeaklyReachable()
}
}
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,
changeType: ControllerChangeType
) {
hasExited = !changeType.isEnter
if (controller.isDestroyed) {
controller.expectWeaklyReachable()
}
}
private fun Controller.expectWeaklyReachable() {
AppWatcher.objectWatcher.expectWeaklyReachable(
this,
"A destroyed controller should have only weak references."
)
}
}
fun Controller.watchForLeaks() {
addLifecycleListener(RefWatchingControllerLifecycleListener())
}
-1
View File
@@ -30,7 +30,6 @@
<!-- Drag Dismiss Demo -->
<string name="drag_to_dismiss">Elastic scrolling view - scroll past bounds to dismiss.</string>
<!-- RxLifecycle Demo -->
<string name="rxlifecycle_title">Watch tag %1$s on logcat for a demo.</string>
<string name="next_controller_retain">Push Controller, retaining this View</string>
<string name="next_controller_release">Push Controller, releasing this View</string>
-63
View File
@@ -1,63 +0,0 @@
ext {
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 28
picassoVersion = '2.5.2'
leakCanaryVersion = '1.5.4'
rxJavaVersion = '1.3.8'
rxJava2Version = '2.1.14'
rxLifecycleVersion = '1.0'
rxLifecycle2Version = '2.2.1'
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-beta04"
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:$leakCanaryVersion"
leakCanaryNoOp = "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
rxJava = "io.reactivex:rxjava:$rxJavaVersion"
rxJava2 = "io.reactivex.rxjava2:rxjava:$rxJava2Version"
rxLifecycle2 = "com.trello.rxlifecycle2:rxlifecycle:$rxLifecycle2Version"
rxLifecycleAndroid2 = "com.trello.rxlifecycle2:rxlifecycle-android:$rxLifecycle2Version"
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"
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"
}
+5 -20
View File
@@ -1,25 +1,9 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
VERSION_CODE=3
VERSION_NAME=3.2.1-SNAPSHOT
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME=3.1.1-SNAPSHOT
VERSION_CODE=3
GROUP=com.bluelinelabs
POM_DESCRIPTION=A small, yet full-featured framework that allows building View-based Android applications
POM_URL=https://github.com/bluelinelabs/Conductor
POM_SCM_URL=https://github.com/bluelinelabs/Conductor
@@ -30,4 +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
android.useAndroidX=true
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
-1
View File
@@ -2,7 +2,6 @@ include ':conductor'
include ':conductor-lint'
include ':conductor-modules:viewpager'
include ':conductor-modules:viewpager2'
include ':conductor-modules:rxlifecycle2'
include ':conductor-modules:autodispose'
include ':conductor-modules:arch-components-lifecycle'
include ':conductor-modules:androidx-transition'