Compare commits

...

55 Commits

Author SHA1 Message Date
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
EricKuck 5f138e5d43 Version bump for 3.1.0 release 2021-07-02 11:18:50 -05:00
Paul Woitaschek 03701d05a9 Add basic support for compose (#644)
Includes required lifecycle and saved state owners
2021-07-02 10:23:00 -05:00
Paul Woitaschek a19968e0c9 Update dokka 2021-06-18 08:19:31 +02:00
Paul Woitaschek 5501ab2ac8 Remove the deprecated jcenter repo. 2021-06-17 21:48:52 +02:00
Paul Woitaschek 804fdb615e Update Gradle to 7.1 2021-06-16 12:29:18 +02:00
EricKuck c01b2a74d6 Version bump 2021-02-05 15:33:01 -06:00
EricKuck 8a8622c261 Fixed issue with VP2 routers potentially restoring incorrectly 2021-02-05 13:04:37 -06:00
Eric Kuck 6820aa7d6a Convert demo app to Kotlin w/ ViewBinding (#635) 2020-12-22 13:09:57 -06:00
EricKuck 9ce27e4dee Added proguard rules to keep empty constructors 2020-12-22 11:49:40 -06:00
Eric Kuck 3c8ad0a833 Switch to GitHub Actions for CI 2020-12-21 14:53:11 -06:00
EricKuck a720ac57e8 Move env vars out of travis config 2020-12-21 11:28:31 -06:00
EricKuck 7d6901389b Always attempt to restore child controllers, even if the views might not exist anymore
Likely fixes #631, #632
2020-11-30 22:43:19 -06:00
EricKuck e54e88bf0d Ensure VP2 adapter saves visible controller instance states
Fixes #634
2020-11-30 22:37:00 -06:00
EricKuck 010117603c Ensure all views are removed when VP2 pages are detached 2020-11-06 10:25:49 -06:00
149 changed files with 6290 additions and 6775 deletions
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
#
# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
#
# Adapted from https://coderwall.com/p/9b_lfq and
# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
SLUG="bluelinelabs/Conductor"
JDK="oraclejdk8"
BRANCH="develop"
set -e
if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then
echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'."
elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then
echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'."
elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
echo "Skipping snapshot deployment: was pull request."
elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then
echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'."
else
echo "Deploying snapshot..."
./gradlew clean uploadArchives
echo "Snapshot deployed!"
fi
+38
View File
@@ -0,0 +1,38 @@
name: Test & Publish
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 11
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew test
publish:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
needs: test
steps:
- uses: actions/checkout@v2
- name: set up JDK 11
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew clean uploadArchives
env:
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ 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
+1
View File
@@ -6,3 +6,4 @@
/caches
/gradle.xml
/modules.xml
/compiler.xml
+9 -1
View File
@@ -118,7 +118,15 @@
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="CALL_PARAMETERS_WRAP" value="5" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
-32
View File
@@ -1,32 +0,0 @@
language: android
before_install:
- mkdir "$ANDROID_HOME/licenses" || true
- echo "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
- yes | sdkmanager --update
script:
- ./gradlew test
jdk:
- oraclejdk8
branches:
only:
- develop
- master
after_success:
- .buildscript/deploy_snapshot.sh
cache:
directories:
- $HOME/.gradle
sudo: false
env:
global:
- secure: Px0uj7aMtKUVZVBnoEjHrEHh4cwO2qKJrHHPvwBiDqhwLBaWQIVXYOy0njaYc5o8p96Fv7bMu7NZx/72vMu1+nmTKxgzrMIxvMW6kczBvJUpv6xd1NuC34x+5Xq5gBNOFvb8JarWStcKgIvnFqvBsRUeI1Hsz7Olb8HF+fEo1kShuP18ezSsBkXruw8JuGiU9x0kq4YhZ7vRvFnc3sJX2FL6heuvQsnUWrolUOsKRadNkCibo+Euuls7ExvbbAXN4LEO3rs0G2eBUBbi2wXvTMG9symtItEHTMPO7K+aQfNQnHsY91TYveH/IJM1u5p6OldsUSOUigzpDmpVYW94aLuJaYqc6Ibq3eUws+tv2didOHXZW5zOCFjldDFBIQFPA3fih/wK/JP0taQ0uIu1+2eifvuERarMkGsYlOFe5tJd10ipi+kK5vNxoRwS9kGv5WwP5fVPX2m5XbD2y1LnugCCcAumfNX7NyNBIRqTy7BP34O3EMLZpMxjwSLnUBnYd4V/0LEvoVmbYmrLhWwpojBJmdwe2QknrPuvRErxNujRA1uEVupbU5A6RW1BmrtzSahJYoROI+ayG7UTOSbFN8+DorER1SUXsrOFlawak8yWsoi6OIynTKucrFM3YcBdJ3Su5AIhfBAOASZa6CUa6sn6Zo8mHmDVGKeckvXnLCU=
- secure: 3Dj6roVTO2a9Z8lwlTGzJJ+QGeyIYSuQ/Z6YsYnW3wB9Mw36uWqw9rOmMNIGjjyAlER8bKgalHr90Pus87oaNaIlbEyvq+L+I0FVAwognViFjo/a2apCcq81THoKjT1l0sgGRzvLNDynQe1q2L0tOu21wtBhMLb7FKQYB9+oD3H+rcU63xD2tv3ToJz+j+dccZ9nrtgk0MQ1xAeMtEb4tdq3flKATKhIuDkp9chaDxx/ZGwyhdE0UP29UUyP5Np1QvpFAlAJIZloZPvde2e05fwxTh4rwUCetkfJknDK6WrGiq97WGRXJpfORNuwGn7jxDCtgxcAm9nGF8qmI/v78BhjJ857CfJBTLGv4QI0RszlhXyezJqqRYjCn9S4yx8UAOixVJfJfFNHLqD4MFn41b7j8J3HDJPxNt0t/qYhUMrgrZVosNOUqhwCyQTKDqtrpvmSUbhHpk2+fxZF1GEL5N540rA0OjLmFUUEDSvRQVaa/waeqXrRefOhsXIx20dHs93C6XDffjnxVMKlqtPab2MlHV/23QCuSa6eW+lKyOZ6ZkWLwwDsLbnuD0WJfJpiL1xaTdPJNIDb6kB/Bno4V1O4xrYncEOaA4Vr4amO8le9Q33/QeNrUZSXs/eye5t0f02wCzubuiEDVEeoR/qj5+5CNWv3AWr/f8AgXp80Pzw=
+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.0.0'
def conductorVersion = '3.1.8'
implementation "com.bluelinelabs:conductor:$conductorVersion"
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.0.0'
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.0.0'
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
// ViewPager2 Adapter:
implementation 'com.bluelinelabs:conductor-viewpager2:3.0.0'
// RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.0.0'
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.0.0'
implementation "com.bluelinelabs:conductor-autodispose:$conductorVersion"
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.0.0'
implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
```
**SNAPSHOT**
Just use `3.0.1-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
Just use `3.1.9-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()));
}
+5 -9
View File
@@ -1,14 +1,13 @@
buildscript {
apply from: rootProject.file('dependencies.gradle')
repositories {
jcenter()
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 libs.agp
classpath libs.kotlin.plugin
classpath libs.mvnpublish
classpath libs.dokka
}
}
@@ -16,8 +15,5 @@ allprojects {
repositories {
mavenCentral()
google()
jcenter()
}
}
apply from: rootProject.file('dependencies.gradle')
+8 -8
View File
@@ -1,15 +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.lint
testImplementation rootProject.ext.lintTests
testImplementation libs.junit
testImplementation libs.lint
testImplementation libs.lint.tests
}
jar {
@@ -17,5 +19,3 @@ jar {
attributes('Lint-Registry-v2': 'com.bluelinelabs.conductor.lint.IssueRegistry')
}
}
apply from: rootProject.file('dependencies.gradle')
@@ -1,25 +0,0 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.detector.api.Issue;
import java.util.Arrays;
import java.util.List;
import static com.android.tools.lint.detector.api.ApiKt.CURRENT_API;
@SuppressWarnings({"unused", "UnstableApiUsage"})
public final class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry {
@Override
public List<Issue> getIssues() {
return Arrays.asList(
ControllerIssueDetector.ISSUE,
ControllerChangeHandlerIssueDetector.ISSUE
);
}
@Override
public int getApi() {
return CURRENT_API;
}
}
@@ -0,0 +1,24 @@
package com.bluelinelabs.conductor.lint
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.client.api.IssueRegistry as LintIssueRegistry
@Suppress("UnstableApiUsage", "unused")
class IssueRegistry : LintIssueRegistry() {
override val issues = listOf(
ControllerIssueDetector.ISSUE,
ControllerChangeHandlerIssueDetector.ISSUE
)
override val api: Int = CURRENT_API
private val githubIssueLink = "https://github.com/bluelinelabs/Conductor/issues/new"
override val vendor = Vendor(
vendorName = "Conductor",
feedbackUrl = githubIssueLink,
contact = githubIssueLink
)
}
@@ -1,13 +1,13 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.checks.infrastructure.LintDetectorTest;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
import com.android.tools.lint.checks.infrastructure.TestFile;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
@SuppressWarnings("UnstableApiUsage")
public class ControllerChangeHandlerDetectorTest {
@@ -17,7 +17,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "^\n"
+ "1 errors, 0 warnings\n";
private final LintDetectorTest.TestFile controllerChangeHandlerStub = java(
private final TestFile controllerChangeHandlerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class ControllerChangeHandler {}"
);
@@ -1,13 +1,13 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.checks.infrastructure.LintDetectorTest;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
import com.android.tools.lint.checks.infrastructure.TestFile;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
@SuppressWarnings("UnstableApiUsage")
public class ControllerDetectorTest {
@@ -22,7 +22,7 @@ public class ControllerDetectorTest {
+ "^\n"
+ "1 errors, 0 warnings\n";
private final LintDetectorTest.TestFile controllerStub = java(
private final TestFile controllerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class Controller {}"
);
@@ -1,24 +1,23 @@
apply plugin: 'com.android.library'
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,22 @@
apply plugin: 'com.android.library'
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;
}
}
});
+6 -8
View File
@@ -1,23 +1,21 @@
apply from: rootProject.file('dependencies.gradle')
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
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.");
}
}
};
}
+6 -8
View File
@@ -2,25 +2,23 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
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.roboelectric
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"
@@ -32,9 +32,9 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
private final Controller host;
private int maxPagesToStateSave = Integer.MAX_VALUE;
private Map<Integer, String> tags = new HashMap<>();
private final Map<Integer, String> tags = new HashMap<>();
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<Router> visibleRouters = new SparseArray<>();
private final SparseArray<Router> visibleRouters = new SparseArray<>();
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
private Router currentPrimaryRouter;
@@ -103,7 +103,7 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Router router = (Router)object;
Bundle savedState = new Bundle();
@@ -121,8 +121,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Router router = (Router)object;
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Router router = (Router) object;
if (router != currentPrimaryRouter) {
if (currentPrimaryRouter != null) {
for (RouterTransaction transaction : currentPrimaryRouter.getBackstack()) {
@@ -139,7 +139,7 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
@Override
public boolean isViewFromObject(View view, Object object) {
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
Router router = (Router)object;
final List<RouterTransaction> backstack = router.getBackstack();
for (RouterTransaction transaction : backstack) {
@@ -1,69 +1,31 @@
package com.bluelinelabs.conductor.viewpager
import android.app.Activity
import android.os.Looper.getMainLooper
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.viewpager.widget.ViewPager
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.viewpager.util.TestController
import com.bluelinelabs.conductor.viewpager.util.TestActivity
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StateSaveTests {
private val pager: ViewPager
private val pagerAdapter: RouterPagerAdapter
private val destroyedItems = mutableListOf<Int>()
private val testController = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.testController()
init {
val activityController = Robolectric.buildActivity(Activity::class.java).setup()
val layout = FrameLayout(activityController.get())
activityController.get().setContentView(layout)
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
val controller = TestController()
router.setRoot(with(controller))
pager = ViewPager(activityController.get()).also {
it.id = ViewCompat.generateViewId()
}
layout.addView(pager)
pager.offscreenPageLimit = 1
pagerAdapter = object : RouterPagerAdapter(controller) {
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
router.setRoot(with(TestController()))
}
}
override fun getCount(): Int {
return 20
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
destroyedItems.add(position)
}
}
pager.adapter = pagerAdapter
shadowOf(getMainLooper()).idle()
}
private val pagerAdapter = testController.pagerAdapter
private val pager = testController.pager
private val destroyedItems = testController.destroyedItems
@Test
fun testNoMaxSaves() {
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
// Ensure all non-visible pages are saved
@@ -81,13 +43,11 @@ class StateSaveTests {
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
val firstSelectedItem = pagerAdapter.count / 2
for (i in pagerAdapter.count downTo firstSelectedItem) {
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
var savedPages = pagerAdapter.savedPages
@@ -103,7 +63,6 @@ class StateSaveTests {
val secondSelectedItem = 1
for (i in firstSelectedItem downTo secondSelectedItem) {
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
savedPages = pagerAdapter.savedPages
@@ -0,0 +1,85 @@
package com.bluelinelabs.conductor.viewpager.util
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.viewpager.widget.ViewPager
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.asTransaction
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
class TestActivity : Activity() {
private lateinit var router: Router
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
router = Conductor.attachRouter(
this,
findViewById(android.R.id.content),
savedInstanceState
)
if (!router.hasRootController()) {
router.setRoot(TestController().asTransaction())
}
}
fun testController(): TestController {
return router.backstack.single().controller as TestController
}
}
class TestController : Controller() {
val destroyedItems = mutableListOf<Int>()
lateinit var pagerAdapter: RouterPagerAdapter
lateinit var pager: ViewPager
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
pager = ViewPager(container.context).also {
it.id = ViewCompat.generateViewId()
}
pager.offscreenPageLimit = 1
pagerAdapter = object : RouterPagerAdapter(this) {
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(PageController()))
}
}
override fun getCount(): Int {
return 20
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
destroyedItems.add(position)
}
}
pager.adapter = pagerAdapter
return pager
}
}
class PageController : Controller() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
return FrameLayout(container.context)
}
}
@@ -1,21 +0,0 @@
package com.bluelinelabs.conductor.viewpager.util;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
public class TestController extends Controller {
@Override @NonNull
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
return new FrameLayout(inflater.getContext());
}
}
+8 -10
View File
@@ -1,13 +1,13 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
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 +20,13 @@ android {
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
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,8 +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) {
@@ -89,6 +89,10 @@ abstract class RouterStateAdapter(private val host: Controller) :
super.onViewDetachedFromWindow(holder)
detachRouter(holder)
// Controller has fully detached and destroyed its view reference by now. Remove the leftover
// view from the container.
holder.container.removeAllViews()
}
override fun onViewRecycled(holder: RouterViewHolder) {
@@ -115,6 +119,19 @@ 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()
while (visiblePositions.isNotEmpty()) {
val lastPosition = visiblePositions.removeAt(visiblePositions.lastIndex)
savePage(getItemId(lastPosition), visibleRouters[lastPosition])
if (visiblePositions.isNotEmpty()) {
val firstPosition = visiblePositions.removeAt(0)
savePage(getItemId(firstPosition), visibleRouters[firstPosition])
}
}
return SavedState(
savedPagesKeys = (0 until savedPages.size()).map { savedPages.keyAt(it) },
savedPagesValues = (0 until savedPages.size()).map { savedPages.valueAt(it) },
@@ -137,12 +154,19 @@ 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)!!
// This should have already been handled by onViewRecycled, but it seems like this wasn't
// always reliably called
if (router != holder.currentRouter) {
holder.currentRouter?.let { host.removeChildRouter(it) }
}
holder.currentRouter = router
holder.currentItemId = itemId
if (!router.hasRootController()) {
val routerSavedState = savedPages[position.toLong()]
val routerSavedState = savedPages[itemId]
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState)
savedPages.remove(itemId)
@@ -172,14 +196,7 @@ abstract class RouterStateAdapter(private val host: Controller) :
holder.currentRouter?.let { router ->
router.prepareForHostDetach()
val savedState = Bundle()
router.saveInstanceState(savedState)
savedPages.put(holder.currentItemId, savedState)
savedPageHistory.remove(holder.currentItemId)
savedPageHistory.add(holder.currentItemId)
ensurePagesSaved()
savePage(holder.currentItemId, router)
if (visibleRouters[holder.currentItemPosition] == router) {
visibleRouters.remove(holder.currentItemPosition)
@@ -189,10 +206,21 @@ abstract class RouterStateAdapter(private val host: Controller) :
holder.attached = false
}
private fun savePage(itemId: Long, router: Router) {
val savedState = Bundle()
router.saveInstanceState(savedState)
savedPages.put(itemId, savedState)
savedPageHistory.remove(itemId)
savedPageHistory.add(itemId)
ensurePagesSaved()
}
private fun ensurePagesSaved() {
while (savedPages.size() > maxPagesToStateSave) {
val positionToRemove = savedPageHistory.removeAt(0)
savedPages.remove(positionToRemove)
val routerIdToRemove = savedPageHistory.removeAt(0)
savedPages.remove(routerIdToRemove)
}
}
+11 -9
View File
@@ -2,11 +2,11 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
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,16 +14,18 @@ android {
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
implementation libs.androidx.savedstate.ktx
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.kotest
api rootProject.ext.androidxAnnotations
api kotlinStd
implementation libs.androidx.lifecycle.runtime
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;
@@ -23,6 +22,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.internal.ClassUtils;
import com.bluelinelabs.conductor.internal.OwnViewTreeLifecycleAndRegistry;
import com.bluelinelabs.conductor.internal.RouterRequiringFunc;
import com.bluelinelabs.conductor.internal.ViewAttachHandler;
import com.bluelinelabs.conductor.internal.ViewAttachHandler.ViewAttachListener;
@@ -142,6 +142,7 @@ public abstract class Controller {
this.args = args != null ? args : new Bundle(getClass().getClassLoader());
instanceId = UUID.randomUUID().toString();
ensureRequiredConstructor();
OwnViewTreeLifecycleAndRegistry.Companion.own(this);
}
/**
@@ -213,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.");
@@ -220,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;
}
@@ -228,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);
@@ -691,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);
}
}
@@ -854,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();
@@ -906,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) {
@@ -969,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();
@@ -979,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);
}
@@ -1042,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) {
@@ -1099,7 +1133,7 @@ public abstract class Controller {
});
viewAttachHandler.listenForAttach(view);
}
} else if (retainViewMode == RetainViewMode.RETAIN_DETACH) {
} else {
restoreChildControllerHosts();
}
@@ -1111,7 +1145,7 @@ public abstract class Controller {
if (!childRouter.hasHost()) {
View containerView = view.findViewById(childRouter.getHostId());
if (containerView != null && containerView instanceof ViewGroup) {
if (containerView instanceof ViewGroup) {
childRouter.setHostContainer(this, (ViewGroup) containerView);
childRouter.rebindIfNeeded();
}
@@ -1119,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) {
@@ -1170,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,7 +221,7 @@ 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);
@@ -250,10 +251,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 +295,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 +390,7 @@ public abstract class Router {
*/
@SuppressWarnings("WeakerAccess")
public int getBackstackSize() {
return backstack.size();
return backstack.getSize();
}
/**
@@ -383,7 +398,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 +560,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 +663,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 +714,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 +741,7 @@ public abstract class Router {
@Override
public void run() {
containerFullyAttached = true;
performPendingControllerChanges();
}
});
}
@@ -745,9 +760,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 +781,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 +829,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 +873,14 @@ public abstract class Router {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
if (container != null) {
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
}
} else {
ControllerChangeHandler.executeChange(transaction);
}
@@ -1001,4 +1039,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
}
}
@@ -24,7 +24,8 @@ private constructor(
private var pushControllerChangeHandler: ControllerChangeHandler? = null,
private var popControllerChangeHandler: ControllerChangeHandler? = null,
private var attachedToRouter: Boolean = false,
@RestrictTo(LIBRARY)
@get:RestrictTo(LIBRARY)
@set:RestrictTo(LIBRARY)
var transactionIndex: Int = INVALID_INDEX
) {
@@ -0,0 +1,223 @@
package com.bluelinelabs.conductor.internal
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.R
/**
* This class sets the [ViewTreeLifecycleOwner] and [ViewTreeSavedStateRegistryOwner] which is
* necessary for Jetpack Compose. By setting these, the view state restoration and compose lifecycle
* play together with the lifecycle of the [Controller].
*/
internal class OwnViewTreeLifecycleAndRegistry private constructor(
controller: Controller
) : LifecycleOwner, SavedStateRegistryOwner {
private lateinit var lifecycleRegistry: LifecycleRegistry
private lateinit var savedStateRegistryController: SavedStateRegistryController
private var hasSavedState = false
private var savedRegistryState = Bundle.EMPTY
private val parentChangeListeners = mutableMapOf<String, Controller.LifecycleListener>()
init {
controller.addLifecycleListener(object : Controller.LifecycleListener() {
override fun preCreateView(controller: Controller) {
hasSavedState = false
lifecycleRegistry = LifecycleRegistry(this@OwnViewTreeLifecycleAndRegistry)
savedStateRegistryController = SavedStateRegistryController.create(
this@OwnViewTreeLifecycleAndRegistry
)
savedStateRegistryController.performRestore(savedRegistryState)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun postCreateView(controller: Controller, view: View) {
/**
* If the consumer of the library already has its own [ViewTreeLifecycleOwner] or
* [ViewTreeSavedStateRegistryOwner] set, don't overwrite it but assume that they're doing
* it on purpose.
*/
if (
view.getTag(R.id.view_tree_lifecycle_owner) == null &&
view.getTag(R.id.view_tree_saved_state_registry_owner) == null
) {
ViewTreeLifecycleOwner.set(view, this@OwnViewTreeLifecycleAndRegistry)
ViewTreeSavedStateRegistryOwner.set(
view,
this@OwnViewTreeLifecycleAndRegistry
)
}
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}
override fun postAttach(controller: Controller, view: View) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun onChangeEnd(
changeController: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
// Should only happen if pushing another controller over this one was aborted
if (
controller === changeController &&
changeType.isEnter &&
changeHandler.removesFromViewOnPush() &&
changeController.view?.windowToken != null &&
lifecycleRegistry.currentState == Lifecycle.State.STARTED
) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
}
override fun onChangeStart(
changeController: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType,
) {
pauseOnChangeStart(
targetController = controller,
changeController = changeController,
changeHandler = changeHandler,
changeType = changeType,
)
}
override fun preDetach(controller: Controller, view: View) {
// Should only happen if pushing this controller was aborted
if (lifecycleRegistry.currentState == Lifecycle.State.RESUMED) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
}
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
override fun onSaveInstanceState(controller: Controller, outState: Bundle) {
outState.putBundle(KEY_SAVED_STATE, savedRegistryState)
}
override fun onSaveViewState(controller: Controller, outState: Bundle) {
if (!hasSavedState) {
savedRegistryState = Bundle()
savedStateRegistryController.performSave(savedRegistryState)
}
}
override fun onRestoreInstanceState(controller: Controller, savedInstanceState: Bundle) {
savedRegistryState = savedInstanceState.getBundle(KEY_SAVED_STATE)
}
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 {
override fun onViewAttachedToWindow(v: View?) = Unit
override fun onViewDetachedFromWindow(v: View?) {
parent.removeOnAttachStateChangeListener(this)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
})
} else {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
override fun postContextAvailable(controller: Controller, context: Context) {
listenForParentChangeStart(controller)
}
override fun preContextUnavailable(controller: Controller, context: Context) {
stopListeningForParentChangeStart(controller)
}
})
}
override fun getLifecycle() = lifecycleRegistry
override fun getSavedStateRegistry() = savedStateRegistryController.savedStateRegistry
private fun listenForParentChangeStart(controller: Controller) {
controller.parentController?.let { parent ->
val changeListener = object : Controller.LifecycleListener() {
override fun onChangeStart(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
// No-op on the case where we (the child controller) hasn't yet created a View as our parent is being
// changed out.
if (::lifecycleRegistry.isInitialized) {
pauseOnChangeStart(
targetController = parent,
changeController = controller,
changeHandler = changeHandler,
changeType = changeType,
)
}
}
}
parent.addLifecycleListener(changeListener)
parentChangeListeners[controller.instanceId] = changeListener
listenForParentChangeStart(parent)
}
}
private fun stopListeningForParentChangeStart(controller: Controller) {
controller.parentController?.let { parent ->
parentChangeListeners.remove(parent.instanceId)?.let { listener ->
parent.removeLifecycleListener(listener)
}
}
}
// 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
}
}
companion object {
private const val KEY_SAVED_STATE = "Registry.savedState"
fun own(target: Controller): OwnViewTreeLifecycleAndRegistry {
return OwnViewTreeLifecycleAndRegistry(target)
}
}
}
@@ -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)
}
}
}
@@ -0,0 +1,9 @@
-keepclassmembers public class * extends com.bluelinelabs.conductor.Controller {
public <init>();
public <init>(android.os.Bundle);
}
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
public <init>();
}
@@ -1,69 +0,0 @@
package com.bluelinelabs.conductor;
import com.bluelinelabs.conductor.util.TestController;
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())
}
}
@@ -2,7 +2,6 @@ package com.bluelinelabs.conductor;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -1,270 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleActivityReferenceTests {
private Router router;
private ActivityProxy activityProxy;
public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) {
activityProxy = new ActivityProxy().create(savedInstanceState);
if (includeStartAndResume) {
activityProxy.start().resume();
}
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
router.setPopsLastView(true);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null, true);
}
@Test
public void testSingleControllerActivityOnPush() {
Controller controller = new TestController();
assertNull(controller.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.emptyList(), listener.postDetachReferences);
assertEquals(Collections.emptyList(), listener.postDestroyViewReferences);
assertEquals(Collections.emptyList(), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPush() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
assertNull(child.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.emptyList(), listener.postDetachReferences);
assertEquals(Collections.emptyList(), listener.postDestroyViewReferences);
assertEquals(Collections.emptyList(), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnPop() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(Arrays.asList(true, true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
childRouter.popCurrentController();
assertEquals(Arrays.asList(true, true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnParentPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnDestroy() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnDestroy() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener {
final List<Boolean> changeEndReferences = new ArrayList<>();
final List<Boolean> postCreateViewReferences = new ArrayList<>();
final List<Boolean> postAttachReferences = new ArrayList<>();
final List<Boolean> postDetachReferences = new ArrayList<>();
final List<Boolean> postDestroyViewReferences = new ArrayList<>();
final List<Boolean> postDestroyReferences = new ArrayList<>();
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
changeEndReferences.add(controller.getActivity() != null);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
postCreateViewReferences.add(controller.getActivity() != null);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
postAttachReferences.add(controller.getActivity() != null);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
postDetachReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
postDestroyViewReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroy(@NonNull Controller controller) {
postDestroyReferences.add(controller.getActivity() != null);
}
}
}
@@ -0,0 +1,255 @@
package com.bluelinelabs.conductor
import android.os.Looper.getMainLooper
import android.view.View
import com.bluelinelabs.conductor.Controller.LifecycleListener
import com.bluelinelabs.conductor.util.MockChangeHandler
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.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class ControllerLifecycleActivityReferenceTests {
private val activityController = Robolectric.buildActivity(TestActivity::class.java).setup()
private val activity = activityController.get()
@Test
fun testSingleControllerActivityOnPush() {
val controller = TestController()
Assert.assertNull(controller.activity)
val listener = ActivityReferencingLifecycleListener()
controller.addLifecycleListener(listener)
activity.router.pushController(
controller.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(emptyList<Any>(), listener.postDetachReferences)
Assert.assertEquals(emptyList<Any>(), listener.postDestroyViewReferences)
Assert.assertEquals(emptyList<Any>(), listener.postDestroyReferences)
}
@Test
fun testChildControllerActivityOnPush() {
val parent = TestController()
activity.router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
Assert.assertNull(child.activity)
val listener = ActivityReferencingLifecycleListener()
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter((parent.view!!.findViewById(TestController.VIEW_ID)))
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(emptyList<Any>(), listener.postDetachReferences)
Assert.assertEquals(emptyList<Any>(), listener.postDestroyViewReferences)
Assert.assertEquals(emptyList<Any>(), listener.postDestroyReferences)
}
@Test
fun testSingleControllerActivityOnPop() {
val controller = TestController()
val listener = ActivityReferencingLifecycleListener()
controller.addLifecycleListener(listener)
activity.router.pushController(
controller.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
activity.router.popCurrentController()
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)
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
}
@Test
fun testChildControllerActivityOnPop() {
val parent = TestController()
activity.router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
val listener = ActivityReferencingLifecycleListener()
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
childRouter.popCurrentController()
shadowOf(getMainLooper()).idle()
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)
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
}
@Test
fun testChildControllerActivityOnParentPop() {
val parent = TestController()
activity.router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
val listener = ActivityReferencingLifecycleListener()
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
activity.router.popCurrentController()
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(listOf(true), listener.postDetachReferences)
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
}
@Test
fun testSingleControllerActivityOnDestroy() {
val controller = TestController()
val listener = ActivityReferencingLifecycleListener()
controller.addLifecycleListener(listener)
activity.router.pushController(
controller.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
activityController.pause().stop().destroy()
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(listOf(true), listener.postDetachReferences)
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
}
@Test
fun testChildControllerActivityOnDestroy() {
val parent = TestController()
activity.router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
val listener = ActivityReferencingLifecycleListener()
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
activityController.pause().stop().destroy()
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(listOf(true), listener.postDetachReferences)
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
}
internal class ActivityReferencingLifecycleListener : LifecycleListener() {
val changeEndReferences = mutableListOf<Boolean>()
val postCreateViewReferences = mutableListOf<Boolean>()
val postAttachReferences = mutableListOf<Boolean>()
val postDetachReferences = mutableListOf<Boolean>()
val postDestroyViewReferences = mutableListOf<Boolean>()
val postDestroyReferences = mutableListOf<Boolean>()
override fun onChangeEnd(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
changeEndReferences.add(controller.activity != null)
}
override fun postCreateView(controller: Controller, view: View) {
postCreateViewReferences.add(controller.activity != null)
}
override fun postAttach(controller: Controller, view: View) {
postAttachReferences.add(controller.activity != null)
}
override fun postDetach(controller: Controller, view: View) {
postDetachReferences.add(controller.activity != null)
}
override fun postDestroyView(controller: Controller) {
postDestroyViewReferences.add(controller.activity != null)
}
override fun postDestroy(controller: Controller) {
postDestroyReferences.add(controller.activity != null)
}
}
}
@@ -1,745 +0,0 @@
package com.bluelinelabs.conductor;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.Controller.RetainViewMode;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.MockChangeHandler.ChangeHandlerListener;
import com.bluelinelabs.conductor.util.TestController;
import com.bluelinelabs.conductor.util.ViewUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleCallbacksTests {
private Router router;
private ActivityProxy activityProxy;
private CallState currentCallState;
public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) {
activityProxy = new ActivityProxy().create(savedInstanceState);
if (includeStartAndResume) {
activityProxy.start().resume();
}
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null, true);
currentCallState = new CallState(false);
}
@Test
public void testNormalLifecycle() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.popChangeHandler(getPopHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
router.popCurrentController();
assertNull(controller.getView());
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityStop() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isDestroying = true;
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(false);
expectedCallState.detachCalls++;
assertCalls(expectedCallState, controller);
assertNotNull(controller.getView());
ViewUtils.reportAttached(controller.getView(), false);
expectedCallState.saveViewStateCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityDestroy() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isDestroying = true;
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(true);
expectedCallState.saveViewStateCalls++;
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
activityProxy.destroy();
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityConfigurationChange() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.tag("root"));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isChangingConfigurations = true;
Bundle bundle = new Bundle();
activityProxy.saveInstanceState(bundle);
expectedCallState.saveViewStateCalls++;
expectedCallState.saveInstanceStateCalls++;
assertCalls(expectedCallState, controller);
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(true);
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
activityProxy.destroy();
expectedCallState.contextUnavailableCalls++;
assertCalls(expectedCallState, controller);
createActivityController(bundle, false);
controller = (TestController)router.getControllerWithTag("root");
expectedCallState.contextAvailableCalls++;
expectedCallState.restoreInstanceStateCalls++;
expectedCallState.restoreViewStateCalls++;
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
// Lifecycle listener isn't attached during restore, grab the current views from the controller for this stuff...
currentCallState.restoreInstanceStateCalls = controller.currentCallState.restoreInstanceStateCalls;
currentCallState.restoreViewStateCalls = controller.currentCallState.restoreViewStateCalls;
currentCallState.changeStartCalls = controller.currentCallState.changeStartCalls;
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.createViewCalls = controller.currentCallState.createViewCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
currentCallState.contextAvailableCalls = controller.currentCallState.contextAvailableCalls;
assertCalls(expectedCallState, controller);
activityProxy.start().resume();
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
expectedCallState.changeEndCalls++;
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
activityProxy.resume();
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityBackground() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.pause();
Bundle bundle = new Bundle();
activityProxy.saveInstanceState(bundle);
expectedCallState.saveInstanceStateCalls++;
expectedCallState.saveViewStateCalls++;
assertCalls(expectedCallState, controller);
activityProxy.resume();
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleCallOrder() {
final TestController testController = new TestController();
final CallState callState = new CallState(false);
testController.addLifecycleListener(new LifecycleListener() {
@Override
public void preCreateView(@NonNull Controller controller) {
callState.createViewCalls++;
assertEquals(1, callState.createViewCalls);
assertEquals(0, testController.currentCallState.createViewCalls);
assertEquals(0, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
callState.createViewCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(0, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(1, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(1, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
callState.destroyViewCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(1, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
callState.destroyViewCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(1, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(2, callState.destroyCalls);
assertEquals(1, testController.currentCallState.destroyCalls);
}
});
router.pushController(RouterTransaction.with(testController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popController(testController);
assertEquals(2, callState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(2, callState.destroyCalls);
}
@Test
public void testLifecycleWhenPopNonCurrentController() {
String controller1Tag = "controller1";
String controller2Tag = "controller2";
String controller3Tag = "controller3";
TestController controller1 = new TestController();
TestController controller2 = new TestController();
TestController controller3 = new TestController();
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
router.pushController(RouterTransaction.with(controller3)
.tag(controller3Tag));
router.popController(controller2);
assertEquals(1, controller2.currentCallState.attachCalls);
assertEquals(1, controller2.currentCallState.createViewCalls);
assertEquals(1, controller2.currentCallState.detachCalls);
assertEquals(1, controller2.currentCallState.destroyViewCalls);
assertEquals(1, controller2.currentCallState.destroyCalls);
assertEquals(1, controller2.currentCallState.contextAvailableCalls);
assertEquals(1, controller2.currentCallState.contextUnavailableCalls);
assertEquals(1, controller2.currentCallState.saveViewStateCalls);
assertEquals(0, controller2.currentCallState.restoreViewStateCalls);
}
@Test
public void testChildLifecycle() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
.popChangeHandler(getPopHandler(expectedCallState, child)));
assertCalls(expectedCallState, child);
parent.removeChildRouter(childRouter);
assertCalls(expectedCallState, child);
}
@Test
public void testChildLifecycle2() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
.popChangeHandler(getPopHandler(expectedCallState, child)));
assertCalls(expectedCallState, child);
router.popCurrentController();
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, child);
}
@Test
public void testChildLifecycleOrderingAfterUnexpectedAttach() {
Controller parent = new TestController();
parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
child.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(new SimpleSwapChangeHandler())
.popChangeHandler(new SimpleSwapChangeHandler()));
assertTrue(parent.isAttached());
assertTrue(child.isAttached());
ViewUtils.reportAttached(parent.getView(), false, true);
assertFalse(parent.isAttached());
assertFalse(child.isAttached());
ViewUtils.reportAttached(child.getView(), true);
assertFalse(parent.isAttached());
assertFalse(child.isAttached());
ViewUtils.reportAttached(parent.getView(), true);
assertTrue(parent.isAttached());
assertTrue(child.isAttached());
}
@Test
public void testChildLifecycleAfterPushAndPop() {
Controller parent = new TestController();
parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(new SimpleSwapChangeHandler())
.popChangeHandler(new SimpleSwapChangeHandler()));
Controller nextController = new TestController();
router.pushController(RouterTransaction.with(nextController));
router.popCurrentController();
assertTrue(parent.isAttached());
assertTrue(child.isAttached());
}
@Test
public void testChildLifecycleAfterPushPopPush() {
Controller parent = new TestController();
parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(new SimpleSwapChangeHandler())
.popChangeHandler(new SimpleSwapChangeHandler()));
Controller nextController = new TestController();
router.pushController(RouterTransaction.with(nextController));
TestController child2 = new TestController();
childRouter.pushController(RouterTransaction.with(child2));
router.popCurrentController();
assertTrue(parent.isAttached());
assertFalse(child.isAttached());
assertTrue(child2.isAttached());
}
private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
public void willStartChange() {
expectedCallState.contextAvailableCalls++;
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didAttachOrDetach() {
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
});
}
private MockChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
public void willStartChange() {
expectedCallState.changeStartCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didAttachOrDetach() {
expectedCallState.destroyViewCalls++;
expectedCallState.detachCalls++;
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
});
}
private void assertCalls(CallState callState, TestController controller) {
assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
assertEquals("Expected call counts and lifecycle call counts do not match.", callState, currentCallState);
}
private void attachLifecycleListener(Controller controller) {
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
currentCallState.changeStartCalls++;
}
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
currentCallState.changeEndCalls++;
}
@Override
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
currentCallState.contextAvailableCalls++;
}
@Override
public void postContextUnavailable(@NonNull Controller controller) {
currentCallState.contextUnavailableCalls++;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
currentCallState.createViewCalls++;
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
currentCallState.attachCalls++;
}
@Override
public void postDestroyView(@NonNull Controller controller) {
currentCallState.destroyViewCalls++;
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
currentCallState.detachCalls++;
}
@Override
public void postDestroy(@NonNull Controller controller) {
currentCallState.destroyCalls++;
}
@Override
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
currentCallState.saveInstanceStateCalls++;
}
@Override
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
currentCallState.restoreInstanceStateCalls++;
}
@Override
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
currentCallState.saveViewStateCalls++;
}
@Override
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
currentCallState.restoreViewStateCalls++;
}
});
}
}
@@ -0,0 +1,702 @@
package com.bluelinelabs.conductor
import android.content.Context
import android.os.Bundle
import android.os.Looper.getMainLooper
import android.view.View
import com.bluelinelabs.conductor.Controller.LifecycleListener
import com.bluelinelabs.conductor.Controller.RetainViewMode
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
import com.bluelinelabs.conductor.util.CallState
import com.bluelinelabs.conductor.util.MockChangeHandler
import com.bluelinelabs.conductor.util.MockChangeHandler.ChangeHandlerListener
import com.bluelinelabs.conductor.util.TestActivity
import com.bluelinelabs.conductor.util.ViewUtils
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.android.controller.ActivityController
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class ControllerLifecycleCallbacksTests {
private lateinit var activityController: ActivityController<TestActivity>
private lateinit var currentCallState: CallState
private fun createActivityController(savedInstanceState: Bundle?, includeStartAndResume: Boolean) {
activityController = Robolectric.buildActivity(TestActivity::class.java)
activityController.create(savedInstanceState)
if (savedInstanceState != null) {
activityController.restoreInstanceState(savedInstanceState)
}
if (includeStartAndResume) {
activityController
.start()
.postCreate(savedInstanceState)
.resume()
.visible()
}
if (!activityController.get().router.hasRootController()) {
activityController.get().router.setRoot(TestController().asTransaction())
}
}
@Before
fun setup() {
createActivityController(null, true)
currentCallState = CallState(false)
}
@Test
fun testNormalLifecycle() {
val controller = TestController()
attachLifecycleListener(controller)
val expectedCallState = CallState(false)
assertCalls(expectedCallState, controller)
activityController.get().router.pushController(
controller.asTransaction(
pushChangeHandler = getPushHandler(expectedCallState, controller),
popChangeHandler = getPopHandler(expectedCallState, controller)
)
)
assertCalls(expectedCallState, controller)
activityController.get().router.popCurrentController()
Assert.assertNull(controller.view)
assertCalls(expectedCallState, controller)
}
@Test
fun testLifecycleWithActivityStop() {
val controller = TestController()
attachLifecycleListener(controller)
val expectedCallState = CallState(false)
assertCalls(expectedCallState, controller)
activityController.get().router.pushController(
controller.asTransaction(
pushChangeHandler = getPushHandler(expectedCallState, controller)
)
)
assertCalls(expectedCallState, controller)
activityController.get().destroying = true
activityController.pause()
assertCalls(expectedCallState, controller)
activityController.stop()
expectedCallState.detachCalls++
assertCalls(expectedCallState, controller)
Assert.assertNotNull(controller.view)
ViewUtils.reportAttached(controller.view, false)
expectedCallState.saveViewStateCalls++
expectedCallState.destroyViewCalls++
assertCalls(expectedCallState, controller)
}
@Test
fun testLifecycleWithActivityDestroy() {
val controller = TestController()
attachLifecycleListener(controller)
val expectedCallState = CallState(false)
assertCalls(expectedCallState, controller)
activityController.get().router.pushController(
controller.asTransaction(
pushChangeHandler = getPushHandler(expectedCallState, controller)
)
)
assertCalls(expectedCallState, controller)
activityController.get().destroying = true
activityController.pause()
assertCalls(expectedCallState, controller)
activityController.stop()
expectedCallState.detachCalls++
assertCalls(expectedCallState, controller)
activityController.destroy()
expectedCallState.destroyViewCalls++
expectedCallState.contextUnavailableCalls++
expectedCallState.destroyCalls++
assertCalls(expectedCallState, controller)
}
@Test
fun testLifecycleWithActivityConfigurationChange() {
var controller = TestController()
attachLifecycleListener(controller)
val expectedCallState = CallState(false)
assertCalls(expectedCallState, controller)
activityController.get().router.pushController(
RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.tag("root")
)
assertCalls(expectedCallState, controller)
activityController.get().changingConfigurations = true
val bundle = Bundle()
activityController.saveInstanceState(bundle)
expectedCallState.saveViewStateCalls++
expectedCallState.saveInstanceStateCalls++
assertCalls(expectedCallState, controller)
activityController.pause()
assertCalls(expectedCallState, controller)
activityController.stop()
expectedCallState.detachCalls++
assertCalls(expectedCallState, controller)
activityController.destroy()
expectedCallState.destroyViewCalls++
expectedCallState.contextUnavailableCalls++
assertCalls(expectedCallState, controller)
createActivityController(bundle, false)
controller = activityController.get().router.getControllerWithTag("root") as TestController
expectedCallState.contextAvailableCalls++
expectedCallState.restoreInstanceStateCalls++
expectedCallState.restoreViewStateCalls++
expectedCallState.changeStartCalls++
expectedCallState.createViewCalls++
// Lifecycle listener isn't attached during restore, grab the current views from the controller for this stuff...
currentCallState.restoreInstanceStateCalls = controller.currentCallState.restoreInstanceStateCalls
currentCallState.restoreViewStateCalls = controller.currentCallState.restoreViewStateCalls
currentCallState.changeStartCalls = controller.currentCallState.changeStartCalls
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls
currentCallState.createViewCalls = controller.currentCallState.createViewCalls
currentCallState.attachCalls = controller.currentCallState.attachCalls
currentCallState.contextAvailableCalls = controller.currentCallState.contextAvailableCalls
assertCalls(expectedCallState, controller)
activityController
.start()
.postCreate(bundle)
.resume()
.visible()
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls
currentCallState.attachCalls = controller.currentCallState.attachCalls
expectedCallState.changeEndCalls++
expectedCallState.attachCalls++
assertCalls(expectedCallState, controller)
activityController.resume()
assertCalls(expectedCallState, controller)
}
@Test
fun testLifecycleWithActivityBackground() {
val controller = TestController()
attachLifecycleListener(controller)
val expectedCallState = CallState(false)
assertCalls(expectedCallState, controller)
activityController.get().router.pushController(
controller.asTransaction(
pushChangeHandler = getPushHandler(expectedCallState, controller)
)
)
assertCalls(expectedCallState, controller)
activityController.pause()
val bundle = Bundle()
activityController.saveInstanceState(bundle)
expectedCallState.saveInstanceStateCalls++
expectedCallState.saveViewStateCalls++
assertCalls(expectedCallState, controller)
activityController.resume()
assertCalls(expectedCallState, controller)
}
@Test
fun testLifecycleCallOrder() {
val testController = TestController()
val callState = CallState(false)
testController.addLifecycleListener(object : LifecycleListener() {
override fun preCreateView(controller: Controller) {
callState.createViewCalls++
Assert.assertEquals(1, callState.createViewCalls)
Assert.assertEquals(0, testController.currentCallState.createViewCalls)
Assert.assertEquals(0, callState.attachCalls)
Assert.assertEquals(0, testController.currentCallState.attachCalls)
Assert.assertEquals(0, callState.detachCalls)
Assert.assertEquals(0, testController.currentCallState.detachCalls)
Assert.assertEquals(0, callState.destroyViewCalls)
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun postCreateView(controller: Controller, view: View) {
callState.createViewCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(0, callState.attachCalls)
Assert.assertEquals(0, testController.currentCallState.attachCalls)
Assert.assertEquals(0, callState.detachCalls)
Assert.assertEquals(0, testController.currentCallState.detachCalls)
Assert.assertEquals(0, callState.destroyViewCalls)
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun preAttach(controller: Controller, view: View) {
callState.attachCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(1, callState.attachCalls)
Assert.assertEquals(0, testController.currentCallState.attachCalls)
Assert.assertEquals(0, callState.detachCalls)
Assert.assertEquals(0, testController.currentCallState.detachCalls)
Assert.assertEquals(0, callState.destroyViewCalls)
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun postAttach(controller: Controller, view: View) {
callState.attachCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(1, testController.currentCallState.attachCalls)
Assert.assertEquals(0, callState.detachCalls)
Assert.assertEquals(0, testController.currentCallState.detachCalls)
Assert.assertEquals(0, callState.destroyViewCalls)
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun preDetach(controller: Controller, view: View) {
callState.detachCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(1, testController.currentCallState.attachCalls)
Assert.assertEquals(1, callState.detachCalls)
Assert.assertEquals(0, testController.currentCallState.detachCalls)
Assert.assertEquals(0, callState.destroyViewCalls)
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun postDetach(controller: Controller, view: View) {
callState.detachCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(1, testController.currentCallState.attachCalls)
Assert.assertEquals(2, callState.detachCalls)
Assert.assertEquals(1, testController.currentCallState.detachCalls)
Assert.assertEquals(0, callState.destroyViewCalls)
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun preDestroyView(controller: Controller, view: View) {
callState.destroyViewCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(1, testController.currentCallState.attachCalls)
Assert.assertEquals(2, callState.detachCalls)
Assert.assertEquals(1, testController.currentCallState.detachCalls)
Assert.assertEquals(1, callState.destroyViewCalls)
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun postDestroyView(controller: Controller) {
callState.destroyViewCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(1, testController.currentCallState.attachCalls)
Assert.assertEquals(2, callState.detachCalls)
Assert.assertEquals(1, testController.currentCallState.detachCalls)
Assert.assertEquals(2, callState.destroyViewCalls)
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(0, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun preDestroy(controller: Controller) {
callState.destroyCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(1, testController.currentCallState.attachCalls)
Assert.assertEquals(2, callState.detachCalls)
Assert.assertEquals(1, testController.currentCallState.detachCalls)
Assert.assertEquals(2, callState.destroyViewCalls)
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(1, callState.destroyCalls)
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
}
override fun postDestroy(controller: Controller) {
callState.destroyCalls++
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(1, testController.currentCallState.attachCalls)
Assert.assertEquals(2, callState.detachCalls)
Assert.assertEquals(1, testController.currentCallState.detachCalls)
Assert.assertEquals(2, callState.destroyViewCalls)
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls)
Assert.assertEquals(2, callState.destroyCalls)
Assert.assertEquals(1, testController.currentCallState.destroyCalls)
}
})
activityController.get().router.pushController(
testController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
activityController.get().router.popController(testController)
Assert.assertEquals(2, callState.createViewCalls)
Assert.assertEquals(2, callState.attachCalls)
Assert.assertEquals(2, callState.detachCalls)
Assert.assertEquals(2, callState.destroyViewCalls)
Assert.assertEquals(2, callState.destroyCalls)
}
@Test
fun testLifecycleWhenPopNonCurrentController() {
val controller1Tag = "controller1"
val controller2Tag = "controller2"
val controller3Tag = "controller3"
val controller1 = TestController()
val controller2 = TestController()
val controller3 = TestController()
activityController.get().router.pushController(
RouterTransaction.with(controller1).tag(controller1Tag)
)
activityController.get().router.pushController(
RouterTransaction.with(controller2).tag(controller2Tag)
)
activityController.get().router.pushController(
RouterTransaction.with(controller3).tag(controller3Tag)
)
activityController.get().router.popController(controller2)
Assert.assertEquals(1, controller2.currentCallState.attachCalls)
Assert.assertEquals(1, controller2.currentCallState.createViewCalls)
Assert.assertEquals(1, controller2.currentCallState.detachCalls)
Assert.assertEquals(1, controller2.currentCallState.destroyViewCalls)
Assert.assertEquals(1, controller2.currentCallState.destroyCalls)
Assert.assertEquals(1, controller2.currentCallState.contextAvailableCalls)
Assert.assertEquals(1, controller2.currentCallState.contextUnavailableCalls)
Assert.assertEquals(1, controller2.currentCallState.saveViewStateCalls)
Assert.assertEquals(0, controller2.currentCallState.restoreViewStateCalls)
}
@Test
fun testChildLifecycle() {
val parent = TestController()
activityController.get().router.pushController(
parent.asTransaction(pushChangeHandler = MockChangeHandler.defaultHandler())
)
val child = TestController()
attachLifecycleListener(child)
val expectedCallState = CallState(false)
assertCalls(expectedCallState, child)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setRoot(
child.asTransaction(
pushChangeHandler = getPushHandler(expectedCallState, child),
popChangeHandler = getPopHandler(expectedCallState, child)
)
)
assertCalls(expectedCallState, child)
parent.removeChildRouter(childRouter)
assertCalls(expectedCallState, child)
}
@Test
fun testChildLifecycle2() {
val parent = TestController()
activityController.get().router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
attachLifecycleListener(child)
val expectedCallState = CallState(false)
assertCalls(expectedCallState, child)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setRoot(
child.asTransaction(
pushChangeHandler = getPushHandler(expectedCallState, child),
popChangeHandler = getPopHandler(expectedCallState, child)
)
)
assertCalls(expectedCallState, child)
activityController.get().router.popCurrentController()
expectedCallState.detachCalls++
expectedCallState.destroyViewCalls++
expectedCallState.contextUnavailableCalls++
expectedCallState.destroyCalls++
assertCalls(expectedCallState, child)
}
@Test
fun testChildLifecycleOrderingAfterUnexpectedAttach() {
val parent = TestController()
parent.retainViewMode = RetainViewMode.RETAIN_DETACH
activityController.get().router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
child.retainViewMode = RetainViewMode.RETAIN_DETACH
val childRouter = parent.getChildRouter(parent.getView()!!.findViewById(TestController.VIEW_ID))
childRouter.setRoot(
child.asTransaction(
pushChangeHandler = SimpleSwapChangeHandler(),
popChangeHandler = SimpleSwapChangeHandler()
)
)
Assert.assertTrue(parent.isAttached)
Assert.assertTrue(child.isAttached)
ViewUtils.reportAttached(parent.view, false, true)
Assert.assertFalse(parent.isAttached)
Assert.assertFalse(child.isAttached)
ViewUtils.reportAttached(child.view, true)
Assert.assertFalse(parent.isAttached)
Assert.assertFalse(child.isAttached)
ViewUtils.reportAttached(parent.view, true)
Assert.assertTrue(parent.isAttached)
Assert.assertTrue(child.isAttached)
}
@Test
fun testChildLifecycleAfterPushAndPop() {
val parent = TestController()
parent.retainViewMode = RetainViewMode.RETAIN_DETACH
activityController.get().router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setRoot(
child.asTransaction(
pushChangeHandler = SimpleSwapChangeHandler(),
popChangeHandler = SimpleSwapChangeHandler()
)
)
val nextController = TestController()
activityController.get().router.pushController(nextController.asTransaction())
activityController.get().router.popCurrentController()
shadowOf(getMainLooper()).idle()
Assert.assertTrue(parent.isAttached)
Assert.assertTrue(child.isAttached)
}
@Test
fun testChildLifecycleAfterPushPopPush() {
val parent = TestController()
parent.retainViewMode = RetainViewMode.RETAIN_DETACH
activityController.get().router.pushController(
parent.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val child = TestController()
val childRouter = parent.getChildRouter(parent.getView()!!.findViewById(TestController.VIEW_ID))
childRouter.setRoot(
child.asTransaction(
pushChangeHandler = SimpleSwapChangeHandler(),
popChangeHandler = SimpleSwapChangeHandler()
)
)
val nextController = TestController()
activityController.get().router.pushController(nextController.asTransaction())
val child2 = TestController()
childRouter.pushController(child2.asTransaction())
activityController.get().router.popCurrentController()
shadowOf(getMainLooper()).idle()
Assert.assertTrue(parent.isAttached)
Assert.assertFalse(child.isAttached)
Assert.assertTrue(child2.isAttached)
}
private fun getPushHandler(
expectedCallState: CallState,
controller: TestController
): MockChangeHandler {
return MockChangeHandler.listeningChangeHandler(object : ChangeHandlerListener() {
override fun willStartChange() {
expectedCallState.contextAvailableCalls++
expectedCallState.changeStartCalls++
expectedCallState.createViewCalls++
assertCalls(expectedCallState, controller)
}
override fun didAttachOrDetach() {
expectedCallState.attachCalls++
assertCalls(expectedCallState, controller)
}
override fun didEndChange() {
expectedCallState.changeEndCalls++
assertCalls(expectedCallState, controller)
}
})
}
private fun getPopHandler(
expectedCallState: CallState,
controller: TestController
): MockChangeHandler {
return MockChangeHandler.listeningChangeHandler(object : ChangeHandlerListener() {
override fun willStartChange() {
expectedCallState.changeStartCalls++
assertCalls(expectedCallState, controller)
}
override fun didAttachOrDetach() {
expectedCallState.destroyViewCalls++
expectedCallState.detachCalls++
expectedCallState.contextUnavailableCalls++
expectedCallState.destroyCalls++
assertCalls(expectedCallState, controller)
}
override fun didEndChange() {
expectedCallState.changeEndCalls++
assertCalls(expectedCallState, controller)
}
})
}
private fun assertCalls(callState: CallState, controller: TestController) {
shadowOf(getMainLooper()).idle()
Assert.assertEquals(
"Expected call counts and controller call counts do not match.",
callState,
controller.currentCallState
)
Assert.assertEquals(
"Expected call counts and lifecycle call counts do not match.",
callState,
currentCallState
)
}
private fun attachLifecycleListener(controller: Controller?) {
controller!!.addLifecycleListener(object : LifecycleListener() {
override fun onChangeStart(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
currentCallState.changeStartCalls++
}
override fun onChangeEnd(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
currentCallState.changeEndCalls++
}
override fun postContextAvailable(controller: Controller, context: Context) {
currentCallState.contextAvailableCalls++
}
override fun postContextUnavailable(controller: Controller) {
currentCallState.contextUnavailableCalls++
}
override fun postCreateView(controller: Controller, view: View) {
currentCallState.createViewCalls++
}
override fun postAttach(controller: Controller, view: View) {
currentCallState.attachCalls++
}
override fun postDestroyView(controller: Controller) {
currentCallState.destroyViewCalls++
}
override fun postDetach(controller: Controller, view: View) {
currentCallState.detachCalls++
}
override fun postDestroy(controller: Controller) {
currentCallState.destroyCalls++
}
override fun onSaveInstanceState(controller: Controller, outState: Bundle) {
currentCallState.saveInstanceStateCalls++
}
override fun onRestoreInstanceState(controller: Controller, savedInstanceState: Bundle) {
currentCallState.restoreInstanceStateCalls++
}
override fun onSaveViewState(controller: Controller, outState: Bundle) {
currentCallState.saveViewStateCalls++
}
override fun onRestoreViewState(controller: Controller, savedViewState: Bundle) {
currentCallState.restoreViewStateCalls++
}
})
}
}
@@ -1,412 +0,0 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.RetainViewMode;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.TestController;
import com.bluelinelabs.conductor.util.ViewUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerTests {
private ActivityProxy activityProxy;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testViewRetention() {
Controller controller = new TestController();
controller.setRouter(router);
// Test View getting released w/ RELEASE_DETACH
controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH);
assertNull(controller.getView());
View view = controller.inflate(router.container);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, true);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, false);
assertNull(controller.getView());
// Test View getting retained w/ RETAIN_DETACH
controller.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
view = controller.inflate(router.container);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, true);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, false);
assertNotNull(controller.getView());
// Ensure re-setting RELEASE_DETACH releases
controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH);
assertNull(controller.getView());
}
@Test
public void testActivityResult() {
TestController controller = new TestController();
CallState expectedCallState = new CallState(true);
router.pushController(RouterTransaction.with(controller));
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
router.onActivityResult(1, Activity.RESULT_OK, null);
assertCalls(expectedCallState, controller);
// Ensure starting an activity for result gets us the result back
controller.startActivityForResult(new Intent("action"), 1);
router.onActivityResult(1, Activity.RESULT_OK, null);
expectedCallState.onActivityResultCalls++;
assertCalls(expectedCallState, controller);
// Ensure requesting a result w/o calling startActivityForResult works
controller.registerForActivityResult(2);
router.onActivityResult(2, Activity.RESULT_OK, null);
expectedCallState.onActivityResultCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testActivityResultForChild() {
TestController parent = new TestController();
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
router.onActivityResult(1, Activity.RESULT_OK, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure starting an activity for result gets us the result back
child.startActivityForResult(new Intent("action"), 1);
router.onActivityResult(1, Activity.RESULT_OK, null);
childExpectedCallState.onActivityResultCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure requesting a result w/o calling startActivityForResult works
child.registerForActivityResult(2);
router.onActivityResult(2, Activity.RESULT_OK, null);
childExpectedCallState.onActivityResultCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
}
@Test
public void testPermissionResult() {
final String[] requestedPermissions = new String[] {"test"};
TestController controller = new TestController();
CallState expectedCallState = new CallState(true);
router.pushController(RouterTransaction.with(controller));
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1});
assertCalls(expectedCallState, controller);
// Ensure requesting the permission gets us the result back
try {
controller.requestPermissions(requestedPermissions, 1);
} catch (NoSuchMethodError ignored) { }
router.onRequestPermissionsResult(controller.getInstanceId(), 1, requestedPermissions, new int[] {1});
expectedCallState.onRequestPermissionsResultCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testPermissionResultForChild() {
final String[] requestedPermissions = new String[] {"test"};
TestController parent = new TestController();
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1});
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure requesting the permission gets us the result back
try {
child.requestPermissions(requestedPermissions, 1);
} catch (NoSuchMethodError ignored) { }
router.onRequestPermissionsResult(child.getInstanceId(), 1, requestedPermissions, new int[] {1});
childExpectedCallState.onRequestPermissionsResultCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
}
@Test
public void testOptionsMenu() {
TestController controller = new TestController();
CallState expectedCallState = new CallState(true);
router.pushController(RouterTransaction.with(controller));
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
router.onCreateOptionsMenu(null, null);
assertCalls(expectedCallState, controller);
// Ensure calling onCreateOptionsMenu with a menu works
controller.setHasOptionsMenu(true);
// Ensure it'll still get called back next time onCreateOptionsMenu is called
router.onCreateOptionsMenu(null, null);
expectedCallState.createOptionsMenuCalls++;
assertCalls(expectedCallState, controller);
// Ensure we stop getting them when we hide it
controller.setOptionsMenuHidden(true);
router.onCreateOptionsMenu(null, null);
assertCalls(expectedCallState, controller);
// Ensure we get the callback them when we un-hide it
controller.setOptionsMenuHidden(false);
router.onCreateOptionsMenu(null, null);
expectedCallState.createOptionsMenuCalls++;
assertCalls(expectedCallState, controller);
// Ensure we don't get the callback when we no longer have a menu
controller.setHasOptionsMenu(false);
router.onCreateOptionsMenu(null, null);
assertCalls(expectedCallState, controller);
}
@Test
public void testOptionsMenuForChild() {
TestController parent = new TestController();
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
router.onCreateOptionsMenu(null, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure calling onCreateOptionsMenu with a menu works
child.setHasOptionsMenu(true);
// Ensure it'll still get called back next time onCreateOptionsMenu is called
router.onCreateOptionsMenu(null, null);
childExpectedCallState.createOptionsMenuCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure we stop getting them when we hide it
child.setOptionsMenuHidden(true);
router.onCreateOptionsMenu(null, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure we get the callback them when we un-hide it
child.setOptionsMenuHidden(false);
router.onCreateOptionsMenu(null, null);
childExpectedCallState.createOptionsMenuCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure we don't get the callback when we no longer have a menu
child.setHasOptionsMenu(false);
router.onCreateOptionsMenu(null, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
}
@Test
public void testAddRemoveChildControllers() {
TestController parent = new TestController();
TestController child1 = new TestController();
TestController child2 = new TestController();
router.pushController(RouterTransaction.with(parent));
assertEquals(0, parent.getChildRouters().size());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.setRoot(RouterTransaction.with(child1));
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(1, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child2));
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(2, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(child2, childRouter.getControllers().get(1));
assertEquals(parent, child1.getParentController());
assertEquals(parent, child2.getParentController());
childRouter.popController(child2);
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(1, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
childRouter.popController(child1);
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(0, childRouter.getBackstackSize());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
}
@Test
public void testAddRemoveChildRouters() {
TestController parent = new TestController();
TestController child1 = new TestController();
TestController child2 = new TestController();
router.pushController(RouterTransaction.with(parent));
assertEquals(0, parent.getChildRouters().size());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2));
childRouter1.setRoot(RouterTransaction.with(child1));
childRouter2.setRoot(RouterTransaction.with(child2));
assertEquals(2, parent.getChildRouters().size());
assertEquals(childRouter1, parent.getChildRouters().get(0));
assertEquals(childRouter2, parent.getChildRouters().get(1));
assertEquals(1, childRouter1.getBackstackSize());
assertEquals(1, childRouter2.getBackstackSize());
assertEquals(child1, childRouter1.getControllers().get(0));
assertEquals(child2, childRouter2.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertEquals(parent, child2.getParentController());
parent.removeChildRouter(childRouter2);
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter1, parent.getChildRouters().get(0));
assertEquals(1, childRouter1.getBackstackSize());
assertEquals(0, childRouter2.getBackstackSize());
assertEquals(child1, childRouter1.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
parent.removeChildRouter(childRouter1);
assertEquals(0, parent.getChildRouters().size());
assertEquals(0, childRouter1.getBackstackSize());
assertEquals(0, childRouter2.getBackstackSize());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
}
@Test
public void testRestoredChildRouterBackstack() {
TestController parent = new TestController();
router.pushController(RouterTransaction.with(parent));
ViewUtils.reportAttached(parent.getView(), true);
RouterTransaction childTransaction1 = RouterTransaction.with(new TestController());
RouterTransaction childTransaction2 = RouterTransaction.with(new TestController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
childRouter.setPopsLastView(true);
childRouter.setRoot(childTransaction1);
childRouter.pushController(childTransaction2);
Bundle savedState = new Bundle();
childRouter.saveInstanceState(savedState);
parent.removeChildRouter(childRouter);
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
assertEquals(0, childRouter.getBackstackSize());
childRouter.restoreInstanceState(savedState);
childRouter.rebindIfNeeded();
assertEquals(2, childRouter.getBackstackSize());
RouterTransaction restoredChildTransaction1 = childRouter.getBackstack().get(0);
RouterTransaction restoredChildTransaction2 = childRouter.getBackstack().get(1);
assertEquals(childTransaction1.getTransactionIndex(), restoredChildTransaction1.getTransactionIndex());
assertEquals(childTransaction1.controller().getInstanceId(), restoredChildTransaction1.controller().getInstanceId());
assertEquals(childTransaction2.getTransactionIndex(), restoredChildTransaction2.getTransactionIndex());
assertEquals(childTransaction2.controller().getInstanceId(), restoredChildTransaction2.controller().getInstanceId());
assertTrue(parent.handleBack());
assertEquals(1, childRouter.getBackstackSize());
assertEquals(restoredChildTransaction1, childRouter.getBackstack().get(0));
assertTrue(parent.handleBack());
assertEquals(0, childRouter.getBackstackSize());
}
private void assertCalls(CallState callState, TestController controller) {
assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
}
}
@@ -0,0 +1,524 @@
package com.bluelinelabs.conductor
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Looper
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.SubMenu
import com.bluelinelabs.conductor.Controller.RetainViewMode
import com.bluelinelabs.conductor.util.AttachFakingFrameLayout
import com.bluelinelabs.conductor.util.CallState
import com.bluelinelabs.conductor.util.TestActivity
import com.bluelinelabs.conductor.util.ViewUtils
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.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class ControllerTests {
private val router = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.router
@Test
fun testViewRetention() {
val controller = TestController()
controller.setRouter(router)
// Test View getting released w/ RELEASE_DETACH
controller.retainViewMode = RetainViewMode.RELEASE_DETACH
Assert.assertNull(controller.getView())
var view = controller.inflate(router.container)
Assert.assertNotNull(controller.getView())
ViewUtils.reportAttached(view, true)
Assert.assertNotNull(controller.getView())
ViewUtils.reportAttached(view, false)
Assert.assertNull(controller.getView())
// Test View getting retained w/ RETAIN_DETACH
controller.retainViewMode = RetainViewMode.RETAIN_DETACH
view = controller.inflate(router.container)
Assert.assertNotNull(controller.getView())
ViewUtils.reportAttached(view, true)
Assert.assertNotNull(controller.getView())
ViewUtils.reportAttached(view, false)
Assert.assertNotNull(controller.getView())
// Ensure re-setting RELEASE_DETACH releases
controller.retainViewMode = RetainViewMode.RELEASE_DETACH
Assert.assertNull(controller.getView())
}
@Test
fun testActivityResult() {
val controller = TestController()
val expectedCallState = CallState(true)
router.pushController(controller.asTransaction())
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
router.onActivityResult(1, Activity.RESULT_OK, null)
assertCalls(expectedCallState, controller)
// Ensure starting an activity for result gets us the result back
controller.startActivityForResult(Intent("action"), 1)
router.onActivityResult(1, Activity.RESULT_OK, null)
expectedCallState.onActivityResultCalls++
assertCalls(expectedCallState, controller)
// Ensure requesting a result w/o calling startActivityForResult works
controller.registerForActivityResult(2)
router.onActivityResult(2, Activity.RESULT_OK, null)
expectedCallState.onActivityResultCalls++
assertCalls(expectedCallState, controller)
}
@Test
fun testActivityResultForChild() {
val parent = TestController()
val child = TestController()
router.pushController(parent.asTransaction())
val childContainer = parent.view!!.findViewById<AttachFakingFrameLayout>(TestController.VIEW_ID)
childContainer.setAttached(true)
parent.getChildRouter(childContainer)
.setRoot(child.asTransaction())
val childExpectedCallState = CallState(true)
val parentExpectedCallState = CallState(true)
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
router.onActivityResult(1, Activity.RESULT_OK, null)
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure starting an activity for result gets us the result back
child.startActivityForResult(Intent("action"), 1)
router.onActivityResult(1, Activity.RESULT_OK, null)
childExpectedCallState.onActivityResultCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure requesting a result w/o calling startActivityForResult works
child.registerForActivityResult(2)
router.onActivityResult(2, Activity.RESULT_OK, null)
childExpectedCallState.onActivityResultCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
}
@Test
fun testPermissionResult() {
val requestedPermissions = arrayOf("test")
val controller = TestController()
val expectedCallState = CallState(true)
router.pushController(controller.asTransaction())
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, intArrayOf(1))
assertCalls(expectedCallState, controller)
// Ensure requesting the permission gets us the result back
try {
controller.requestPermissions(requestedPermissions, 1)
} catch (ignored: NoSuchMethodError) { }
router.onRequestPermissionsResult(
controller.instanceId,
1,
requestedPermissions,
intArrayOf(1)
)
expectedCallState.onRequestPermissionsResultCalls++
assertCalls(expectedCallState, controller)
}
@Test
fun testPermissionResultForChild() {
val requestedPermissions = arrayOf("test")
val parent = TestController()
val child = TestController()
router.pushController(parent.asTransaction())
val childContainer = parent.view!!.findViewById<AttachFakingFrameLayout>(TestController.VIEW_ID)
childContainer.setAttached(true)
parent.getChildRouter(childContainer)
.setRoot(child.asTransaction())
val childExpectedCallState = CallState(true)
val parentExpectedCallState = CallState(true)
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, intArrayOf(1))
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure requesting the permission gets us the result back
try {
child.requestPermissions(requestedPermissions, 1)
} catch (ignored: NoSuchMethodError) { }
router.onRequestPermissionsResult(child.instanceId, 1, requestedPermissions, intArrayOf(1))
childExpectedCallState.onRequestPermissionsResultCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
}
@Test
fun testOptionsMenu() {
val controller = TestController()
val expectedCallState = CallState(true)
router.pushController(controller.asTransaction())
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
assertCalls(expectedCallState, controller)
// Ensure calling onCreateOptionsMenu with a menu works
controller.setHasOptionsMenu(true)
expectedCallState.createOptionsMenuCalls++
assertCalls(expectedCallState, controller)
// Ensure it'll still get called back next time onCreateOptionsMenu is called
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
expectedCallState.createOptionsMenuCalls++
assertCalls(expectedCallState, controller)
// Ensure we stop getting them when we hide it
controller.setOptionsMenuHidden(true)
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
assertCalls(expectedCallState, controller)
// Ensure we get the callback them when we un-hide it
controller.setOptionsMenuHidden(false)
expectedCallState.createOptionsMenuCalls++
assertCalls(expectedCallState, controller)
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
expectedCallState.createOptionsMenuCalls++
assertCalls(expectedCallState, controller)
// Ensure we don't get the callback when we no longer have a menu
controller.setHasOptionsMenu(false)
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
assertCalls(expectedCallState, controller)
}
@Test
fun testOptionsMenuForChild() {
val parent = TestController()
val child = TestController()
router.pushController(parent.asTransaction())
val childContainer = parent.view!!.findViewById<AttachFakingFrameLayout>(TestController.VIEW_ID)
childContainer.setAttached(true)
parent.getChildRouter(childContainer)
.setRoot(child.asTransaction())
val childExpectedCallState = CallState(true)
val parentExpectedCallState = CallState(true)
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure calling onCreateOptionsMenu with a menu works
child.setHasOptionsMenu(true)
childExpectedCallState.createOptionsMenuCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure it'll still get called back next time onCreateOptionsMenu is called
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
childExpectedCallState.createOptionsMenuCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure we stop getting them when we hide it
child.setOptionsMenuHidden(true)
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure we get the callback them when we un-hide it
child.setOptionsMenuHidden(false)
childExpectedCallState.createOptionsMenuCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
childExpectedCallState.createOptionsMenuCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
// Ensure we don't get the callback when we no longer have a menu
child.setHasOptionsMenu(false)
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
}
@Test
fun testAddRemoveChildControllers() {
val parent = TestController()
val child1 = TestController()
val child2 = TestController()
router.pushController(parent.asTransaction())
Assert.assertEquals(0, parent.childRouters.size)
Assert.assertNull(child1.parentController)
Assert.assertNull(child2.parentController)
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
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])
Assert.assertEquals(1, childRouter.backstackSize)
Assert.assertEquals(child1, childRouter.controllers[0])
Assert.assertEquals(parent, child1.parentController)
Assert.assertNull(child2.parentController)
childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.pushController(child2.asTransaction())
Assert.assertEquals(1, parent.childRouters.size)
Assert.assertEquals(childRouter, parent.childRouters[0])
Assert.assertEquals(2, childRouter.backstackSize)
Assert.assertEquals(child1, childRouter.controllers[0])
Assert.assertEquals(child2, childRouter.controllers[1])
Assert.assertEquals(parent, child1.parentController)
Assert.assertEquals(parent, child2.parentController)
childRouter.popController(child2)
Assert.assertEquals(1, parent.childRouters.size)
Assert.assertEquals(childRouter, parent.childRouters[0])
Assert.assertEquals(1, childRouter.backstackSize)
Assert.assertEquals(child1, childRouter.controllers[0])
Assert.assertEquals(parent, child1.parentController)
Assert.assertNull(child2.parentController)
childRouter.popController(child1)
shadowOf(Looper.getMainLooper()).idle()
Assert.assertEquals(1, parent.childRouters.size)
Assert.assertEquals(childRouter, parent.childRouters[0])
Assert.assertEquals(0, childRouter.backstackSize)
Assert.assertNull(child1.parentController)
Assert.assertNull(child2.parentController)
}
@Test
fun testAddRemoveChildRouters() {
val parent = TestController()
val child1 = TestController()
val child2 = TestController()
router.pushController(parent.asTransaction())
Assert.assertEquals(0, parent.childRouters.size)
Assert.assertNull(child1.parentController)
Assert.assertNull(child2.parentController)
val childRouter1 = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
val childRouter2 = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_2))
childRouter1.setRoot(child1.asTransaction())
childRouter2.setRoot(child2.asTransaction())
Assert.assertEquals(2, parent.childRouters.size)
Assert.assertEquals(childRouter1, parent.childRouters[0])
Assert.assertEquals(childRouter2, parent.childRouters[1])
Assert.assertEquals(1, childRouter1.backstackSize)
Assert.assertEquals(1, childRouter2.backstackSize)
Assert.assertEquals(child1, childRouter1.controllers[0])
Assert.assertEquals(child2, childRouter2.controllers[0])
Assert.assertEquals(parent, child1.parentController)
Assert.assertEquals(parent, child2.parentController)
parent.removeChildRouter(childRouter2)
shadowOf(Looper.getMainLooper()).idle()
Assert.assertEquals(1, parent.childRouters.size)
Assert.assertEquals(childRouter1, parent.childRouters[0])
Assert.assertEquals(1, childRouter1.backstackSize)
Assert.assertEquals(0, childRouter2.backstackSize)
Assert.assertEquals(child1, childRouter1.controllers[0])
Assert.assertEquals(parent, child1.parentController)
Assert.assertNull(child2.parentController)
parent.removeChildRouter(childRouter1)
Assert.assertEquals(0, parent.childRouters.size)
Assert.assertEquals(0, childRouter1.backstackSize)
Assert.assertEquals(0, childRouter2.backstackSize)
Assert.assertNull(child1.parentController)
Assert.assertNull(child2.parentController)
}
@Test
fun testRestoredChildRouterBackstack() {
val parent = TestController()
router.pushController(parent.asTransaction())
ViewUtils.reportAttached(parent.view, true)
val childTransaction1 = TestController().asTransaction()
val childTransaction2 = TestController().asTransaction()
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.setRoot(childTransaction1)
childRouter.pushController(childTransaction2)
val savedState = Bundle()
childRouter.saveInstanceState(savedState)
parent.removeChildRouter(childRouter)
childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
Assert.assertEquals(0, childRouter.backstackSize)
childRouter.restoreInstanceState(savedState)
childRouter.rebindIfNeeded()
Assert.assertEquals(2, childRouter.backstackSize)
val restoredChildTransaction1 = childRouter.getBackstack()[0]
val restoredChildTransaction2 = childRouter.getBackstack()[1]
Assert.assertEquals(
childTransaction1.transactionIndex,
restoredChildTransaction1.transactionIndex
)
Assert.assertEquals(
childTransaction1.controller.getInstanceId(),
restoredChildTransaction1.controller.getInstanceId()
)
Assert.assertEquals(
childTransaction2.transactionIndex,
restoredChildTransaction2.transactionIndex
)
Assert.assertEquals(
childTransaction2.controller.getInstanceId(),
restoredChildTransaction2.controller.getInstanceId()
)
Assert.assertTrue(parent.handleBack())
Assert.assertEquals(1, childRouter.backstackSize)
Assert.assertEquals(restoredChildTransaction1, childRouter.getBackstack()[0])
Assert.assertTrue(parent.handleBack())
Assert.assertEquals(0, childRouter.backstackSize)
}
private fun assertCalls(callState: CallState, controller: TestController) {
shadowOf(Looper.getMainLooper()).idle()
Assert.assertEquals(
"Expected call counts and controller call counts do not match.",
callState,
controller.currentCallState
)
}
private fun menu(): Menu {
return object : Menu {
override fun add(p0: CharSequence?): MenuItem {
TODO("Not yet implemented")
}
override fun add(p0: Int): MenuItem {
TODO("Not yet implemented")
}
override fun add(p0: Int, p1: Int, p2: Int, p3: CharSequence?): MenuItem {
TODO("Not yet implemented")
}
override fun add(p0: Int, p1: Int, p2: Int, p3: Int): MenuItem {
TODO("Not yet implemented")
}
override fun addSubMenu(p0: CharSequence?): SubMenu {
TODO("Not yet implemented")
}
override fun addSubMenu(p0: Int): SubMenu {
TODO("Not yet implemented")
}
override fun addSubMenu(p0: Int, p1: Int, p2: Int, p3: CharSequence?): SubMenu {
TODO("Not yet implemented")
}
override fun addSubMenu(p0: Int, p1: Int, p2: Int, p3: Int): SubMenu {
TODO("Not yet implemented")
}
override fun addIntentOptions(
p0: Int,
p1: Int,
p2: Int,
p3: ComponentName?,
p4: Array<out Intent>?,
p5: Intent?,
p6: Int,
p7: Array<out MenuItem>?
): Int {
TODO("Not yet implemented")
}
override fun removeItem(p0: Int) {
TODO("Not yet implemented")
}
override fun removeGroup(p0: Int) {
TODO("Not yet implemented")
}
override fun clear() {
TODO("Not yet implemented")
}
override fun setGroupCheckable(p0: Int, p1: Boolean, p2: Boolean) {
TODO("Not yet implemented")
}
override fun setGroupVisible(p0: Int, p1: Boolean) {
TODO("Not yet implemented")
}
override fun setGroupEnabled(p0: Int, p1: Boolean) {
TODO("Not yet implemented")
}
override fun hasVisibleItems(): Boolean {
TODO("Not yet implemented")
}
override fun findItem(p0: Int): MenuItem {
TODO("Not yet implemented")
}
override fun size(): Int {
TODO("Not yet implemented")
}
override fun getItem(p0: Int): MenuItem {
TODO("Not yet implemented")
}
override fun close() {
TODO("Not yet implemented")
}
override fun performShortcut(p0: Int, p1: KeyEvent?, p2: Int): Boolean {
TODO("Not yet implemented")
}
override fun isShortcutKey(p0: Int, p1: KeyEvent?): Boolean {
TODO("Not yet implemented")
}
override fun performIdentifierAction(p0: Int, p1: Int): Boolean {
TODO("Not yet implemented")
}
override fun setQwertyMode(p0: Boolean) {
TODO("Not yet implemented")
}
}
}
private fun menuInflater(context: Context): MenuInflater {
return MenuInflater(context)
}
}
@@ -4,7 +4,6 @@ import android.os.Bundle;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -1,406 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.AttachFakingFrameLayout;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ReattachCaseTests {
private ActivityProxy activityProxy;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testNeedsAttachingOnPauseAndOrientation() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
sleepWakeDevice();
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
}
@Test
public void testChildNeedsAttachOnPauseAndOrientation() {
final Controller controllerA = new TestController();
final Controller childController = new TestController();
final Controller controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerA.getChildRouter((ViewGroup) controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertTrue(childController.isAttached());
assertFalse(controllerB.isAttached());
sleepWakeDevice();
assertTrue(controllerA.isAttached());
assertTrue(childController.isAttached());
assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertFalse(childController.isAttached());
assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertFalse(childController.isAttached());
assertTrue(childController.getNeedsAttach());
assertTrue(controllerB.isAttached());
}
@Test
public void testChildHandleBackOnOrientation() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
final TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
router.handleBack();
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/86#issuecomment-231381271
@Test
public void testReusedChildRouterHandleBackOnOrientation() {
TestController controllerA = new TestController();
TestController controllerB = new TestController();
TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
router.handleBack();
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/367
@Test
public void testViewIsAttachedAfterStartedActivityIsRecreated() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
assertTrue(controller1.isAttached());
// Lock screen
Bundle bundle = new Bundle();
activityProxy.pause().saveInstanceState(bundle).stop(false);
// Push a 2nd controller, which will rotate the screen once it unlocked
router.pushController(RouterTransaction.with(controller2));
assertTrue(controller2.isAttached());
assertTrue(controller2.getNeedsAttach());
// Unlock screen and rotate
activityProxy.start();
activityProxy.rotate();
assertTrue(controller2.isAttached());
}
@Test
public void testPopMiddleControllerAttaches() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
Controller controller3 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
router.pushController(RouterTransaction.with(controller3));
router.popController(controller2);
assertFalse(controller1.isAttached());
assertFalse(controller2.isAttached());
assertTrue(controller3.isAttached());
controller1 = new TestController();
controller2 = new TestController();
controller3 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
router.pushController(RouterTransaction.with(controller3).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()));
router.popController(controller2);
assertTrue(controller1.isAttached());
assertFalse(controller2.isAttached());
assertTrue(controller3.isAttached());
}
@Test
public void testPendingChanges() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
ActivityProxy activityProxy = new ActivityProxy().create(null);
AttachFakingFrameLayout container = new AttachFakingFrameLayout(activityProxy.getActivity());
container.setNeedDelayPost(true); // to simulate calling posts after resume
activityProxy.setView(container);
Router router = Conductor.attachRouter(activityProxy.getActivity(), container, null);
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
activityProxy.start().resume();
container.setNeedDelayPost(false);
assertTrue(controller2.isAttached());
}
@Test
public void testPendingChangesAfterRotation() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
// first activity
ActivityProxy activityProxy = new ActivityProxy().create(null);
AttachFakingFrameLayout container1 = new AttachFakingFrameLayout(activityProxy.getActivity());
container1.setNeedDelayPost(true); // delay forever as view will be removed
activityProxy.setView(container1);
// first attachRouter: Conductor.attachRouter(activityProxy.getActivity(), container1, null)
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activityProxy.getActivity());
Router router = lifecycleHandler.getRouter(container1, null);
router.setRoot(RouterTransaction.with(controller1));
// setup controllers
router.pushController(RouterTransaction.with(controller2));
// simulate setRequestedOrientation in activity onCreate
activityProxy.start().resume();
Bundle savedState = new Bundle();
activityProxy.saveInstanceState(savedState).pause().stop(true);
// recreate activity and view
activityProxy = new ActivityProxy().create(savedState);
AttachFakingFrameLayout container2 = new AttachFakingFrameLayout(activityProxy.getActivity());
activityProxy.setView(container2);
// second attach router with the same lifecycleHandler (do manually as Roboelectric recreates retained fragments)
// Conductor.attachRouter(activityProxy.getActivity(), container2, savedState);
router = lifecycleHandler.getRouter(container2, savedState);
router.rebindIfNeeded();
activityProxy.start().resume();
assertTrue(controller2.isAttached());
}
@Test
public void testHostAvailableDuringRotation() {
final Controller controllerA = new TestController();
final Controller childControllerA = new TestController();
final Controller controllerB = new TestController();
final Controller childControllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouterA = controllerA.getChildRouter((ViewGroup) controllerA.getView().findViewById(TestController.VIEW_ID));
childRouterA.pushController(RouterTransaction.with(childControllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouterB = controllerB.getChildRouter((ViewGroup) controllerB.getView().findViewById(TestController.VIEW_ID));
childRouterB.pushController(RouterTransaction.with(childControllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
assertNotNull(controllerB.getActivity());
assertNotNull(childControllerB.getActivity());
activityProxy.rotate();
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
assertNotNull(controllerB.getActivity());
assertNotNull(childControllerB.getActivity());
router.rebindIfNeeded();
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
assertNotNull(controllerB.getActivity());
assertNotNull(childControllerB.getActivity());
}
private void sleepWakeDevice() {
activityProxy.saveInstanceState(new Bundle()).pause();
activityProxy.resume();
}
}
@@ -0,0 +1,400 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import android.os.Looper
import com.bluelinelabs.conductor.Conductor.attachRouter
import com.bluelinelabs.conductor.internal.LifecycleHandler
import com.bluelinelabs.conductor.util.ActivityProxy
import com.bluelinelabs.conductor.util.AttachFakingFrameLayout
import com.bluelinelabs.conductor.util.MockChangeHandler
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.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class ReattachCaseTests {
private val activityController = Robolectric.buildActivity(TestActivity::class.java).setup()
private val router = activityController.get().router
@Test
fun testNeedsAttachingOnPauseAndOrientation() {
val controllerA = TestController()
val controllerB = TestController()
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertTrue(controllerA.isAttached)
Assert.assertFalse(controllerB.isAttached)
sleepWakeDevice()
Assert.assertTrue(controllerA.isAttached)
Assert.assertFalse(controllerB.isAttached)
router.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
activityController.configurationChange()
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
}
@Test
fun testChildNeedsAttachOnPauseAndOrientation() {
val controllerA = TestController()
val childController = TestController()
val controllerB = TestController()
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val childRouter = controllerA.getChildRouter(
controllerA.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertTrue(controllerA.isAttached)
Assert.assertTrue(childController.isAttached)
Assert.assertFalse(controllerB.isAttached)
sleepWakeDevice()
Assert.assertTrue(controllerA.isAttached)
Assert.assertTrue(childController.isAttached)
Assert.assertFalse(controllerB.isAttached)
router.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertFalse(controllerA.isAttached)
Assert.assertFalse(childController.isAttached)
Assert.assertTrue(controllerB.isAttached)
activityController.configurationChange()
Assert.assertFalse(controllerA.isAttached)
Assert.assertFalse(childController.isAttached)
Assert.assertTrue(childController.needsAttach)
Assert.assertTrue(controllerB.isAttached)
}
@Test
fun testChildHandleBackOnOrientation() {
val controllerA = TestController()
val controllerB = TestController()
val childController = TestController()
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertTrue(controllerA.isAttached)
Assert.assertFalse(controllerB.isAttached)
Assert.assertFalse(childController.isAttached)
router.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val childRouter = controllerB.getChildRouter(
controllerB.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertTrue(childController.isAttached)
activityController.configurationChange()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertTrue(childController.isAttached)
router.handleBack()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertFalse(childController.isAttached)
router.handleBack()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertTrue(controllerA.isAttached)
Assert.assertFalse(controllerB.isAttached)
Assert.assertFalse(childController.isAttached)
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/86#issuecomment-231381271
@Test
fun testReusedChildRouterHandleBackOnOrientation() {
val controllerA = TestController()
val controllerB = TestController()
var childController = TestController()
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertTrue(controllerA.isAttached)
Assert.assertFalse(controllerB.isAttached)
Assert.assertFalse(childController.isAttached)
router.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val childRouter = controllerB.getChildRouter(
controllerB.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertTrue(childController.isAttached)
router.handleBack()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertFalse(childController.isAttached)
childController = TestController()
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertTrue(childController.isAttached)
activityController.configurationChange()
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertTrue(childController.isAttached)
router.handleBack()
childController = TestController()
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertTrue(childController.isAttached)
router.handleBack()
Assert.assertFalse(controllerA.isAttached)
Assert.assertTrue(controllerB.isAttached)
Assert.assertFalse(childController.isAttached)
router.handleBack()
Assert.assertTrue(controllerA.isAttached)
Assert.assertFalse(controllerB.isAttached)
Assert.assertFalse(childController.isAttached)
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/367
@Test
fun testViewIsAttachedAfterStartedActivityIsRecreated() {
val controller1 = TestController()
val controller2 = TestController()
router.setRoot(controller1.asTransaction())
Assert.assertTrue(controller1.isAttached)
// Lock screen
val bundle = Bundle()
activityController.pause().saveInstanceState(bundle).stop()
// Push a 2nd controller, which will rotate the screen once it unlocked
router.pushController(controller2.asTransaction())
Assert.assertTrue(controller2.isAttached)
Assert.assertTrue(controller2.needsAttach)
// Unlock screen and rotate
activityController.start()
activityController.configurationChange()
Assert.assertTrue(controller2.isAttached)
}
@Test
fun testPopMiddleControllerAttaches() {
var controller1 = TestController()
var controller2 = TestController()
var controller3 = TestController()
router.setRoot(controller1.asTransaction())
router.pushController(controller2.asTransaction())
router.pushController(controller3.asTransaction())
router.popController(controller2)
Assert.assertFalse(controller1.isAttached)
Assert.assertFalse(controller2.isAttached)
Assert.assertTrue(controller3.isAttached)
controller1 = TestController()
controller2 = TestController()
controller3 = TestController()
router.setRoot(controller1.asTransaction())
router.pushController(controller2.asTransaction())
router.pushController(
controller3.asTransaction(
pushChangeHandler = MockChangeHandler.noRemoveViewOnPushHandler()
)
)
router.popController(controller2)
Assert.assertTrue(controller1.isAttached())
Assert.assertFalse(controller2.isAttached())
Assert.assertTrue(controller3.isAttached())
}
@Test
fun testPendingChanges() {
val controller1 = TestController()
val controller2 = TestController()
val activityProxy = ActivityProxy().create(null)
val container = AttachFakingFrameLayout(activityProxy.activity)
container.setNeedDelayPost(true) // to simulate calling posts after resume
activityProxy.view = container
val router = attachRouter(activityProxy.activity, container, null)
router.setRoot(controller1.asTransaction())
router.pushController(controller2.asTransaction())
activityProxy.start().resume()
container.setNeedDelayPost(false)
Assert.assertTrue(controller2.isAttached)
}
@Test
fun testPendingChangesAfterRotation() {
val controller1 = TestController()
val controller2 = TestController()
// first activity
var activityProxy = ActivityProxy().create(null)
val container1 = AttachFakingFrameLayout(activityProxy.activity)
container1.setNeedDelayPost(true) // delay forever as view will be removed
activityProxy.view = container1
// first attachRouter: Conductor.attachRouter(activityProxy.getActivity(), container1, null)
val lifecycleHandler = LifecycleHandler.install(activityProxy.activity)
var router = lifecycleHandler.getRouter(container1, null)
router.setRoot(controller1.asTransaction())
// setup controllers
router.pushController(controller2.asTransaction())
// simulate setRequestedOrientation in activity onCreate
activityProxy.start().resume()
val savedState = Bundle()
activityProxy.saveInstanceState(savedState).pause().stop(true)
// recreate activity and view
activityProxy = ActivityProxy().create(savedState)
val container2 = AttachFakingFrameLayout(activityProxy.activity)
activityProxy.view = container2
// second attach router with the same lifecycleHandler (do manually as robolectric recreates retained fragments)
// Conductor.attachRouter(activityProxy.getActivity(), container2, savedState);
router = lifecycleHandler.getRouter(container2, savedState)
router.rebindIfNeeded()
activityProxy.start().resume()
Assert.assertTrue(controller2.isAttached)
}
@Test
fun testHostAvailableDuringRotation() {
val controllerA = TestController()
val childControllerA = TestController()
val controllerB = TestController()
val childControllerB = TestController()
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val childRouterA = controllerA.getChildRouter(
controllerA.view!!.findViewById(TestController.VIEW_ID)
)
childRouterA.pushController(
childControllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertNotNull(controllerA.activity)
Assert.assertNotNull(childControllerA.activity)
router.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
val childRouterB = controllerB.getChildRouter(
controllerB.view!!.findViewById(
TestController.VIEW_ID
)
)
childRouterB.pushController(
childControllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertNotNull(controllerA.activity)
Assert.assertNotNull(childControllerA.activity)
Assert.assertNotNull(controllerB.activity)
Assert.assertNotNull(childControllerB.activity)
activityController.configurationChange()
Assert.assertNotNull(controllerA.activity)
Assert.assertNotNull(childControllerA.activity)
Assert.assertNotNull(controllerB.activity)
Assert.assertNotNull(childControllerB.activity)
}
private fun sleepWakeDevice() {
activityController.saveInstanceState(Bundle()).pause()
activityController.resume()
}
}
@@ -1,354 +0,0 @@
package com.bluelinelabs.conductor;
import android.view.View;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RouterChangeHandlerTests {
private Router router;
@Before
public void setup() {
ActivityProxy activityProxy = new ActivityProxy().create(null).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), null);
}
@Test
public void testSetRootHandler() {
MockChangeHandler handler = MockChangeHandler.taggedHandler("root", true);
TestController rootController = new TestController();
router.setRoot(RouterTransaction.with(rootController).pushChangeHandler(handler));
assertTrue(rootController.changeHandlerHistory.isValidHistory);
assertNull(rootController.changeHandlerHistory.latestFromView());
assertNotNull(rootController.changeHandlerHistory.latestToView());
assertEquals(rootController.getView(), rootController.changeHandlerHistory.latestToView());
assertTrue(rootController.changeHandlerHistory.latestIsPush());
assertEquals(handler.tag, rootController.changeHandlerHistory.latestChangeHandler().tag);
}
@Test
public void testPushPopHandlers() {
TestController rootController = new TestController();
router.setRoot(RouterTransaction.with(rootController).pushChangeHandler(MockChangeHandler.defaultHandler()));
View rootView = rootController.getView();
MockChangeHandler pushHandler = MockChangeHandler.taggedHandler("push", true);
MockChangeHandler popHandler = MockChangeHandler.taggedHandler("pop", true);
TestController pushController = new TestController();
router.pushController(RouterTransaction.with(pushController).pushChangeHandler(pushHandler).popChangeHandler(popHandler));
assertTrue(rootController.changeHandlerHistory.isValidHistory);
assertTrue(pushController.changeHandlerHistory.isValidHistory);
assertNotNull(pushController.changeHandlerHistory.latestFromView());
assertNotNull(pushController.changeHandlerHistory.latestToView());
assertEquals(rootView, pushController.changeHandlerHistory.latestFromView());
assertEquals(pushController.getView(), pushController.changeHandlerHistory.latestToView());
assertTrue(pushController.changeHandlerHistory.latestIsPush());
assertEquals(pushHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag);
View pushView = pushController.getView();
router.popController(pushController);
assertNotNull(pushController.changeHandlerHistory.latestFromView());
assertNotNull(pushController.changeHandlerHistory.latestToView());
assertEquals(pushView, pushController.changeHandlerHistory.fromViewAt(1));
assertEquals(rootController.getView(), pushController.changeHandlerHistory.latestToView());
assertFalse(pushController.changeHandlerHistory.latestIsPush());
assertEquals(popHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag);
}
@Test
public void testResetRootHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newRootController = new TestController();
MockChangeHandler newRootHandler = MockChangeHandler.taggedHandler("newRootHandler", true);
router.setRoot(RouterTransaction.with(newRootController).pushChangeHandler(newRootHandler));
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newRootController.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(1, newRootController.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newRootController.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newRootController.changeHandlerHistory.latestToView());
assertEquals(newRootController.getView(), newRootController.changeHandlerHistory.latestToView());
assertEquals(initialView1, newRootController.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, newRootController.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newRootController.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newController1 = new TestController();
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2)
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newController1.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(0, newController1.changeHandlerHistory.size());
assertEquals(1, newController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newController2.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView());
assertEquals(initialView1, newController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController2.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackWithTwoVisibleHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newController1 = new TestController();
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
MockChangeHandler pushController2Handler = MockChangeHandler.noRemoveViewOnPushHandler("pushController2");
List<RouterTransaction> newBackstack = Arrays.asList(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2).pushChangeHandler(pushController2Handler)
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newController1.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(2, newController1.changeHandlerHistory.size());
assertEquals(1, newController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newController1.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), newController1.changeHandlerHistory.toViewAt(0));
assertEquals(newController2.getView(), newController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, newController1.changeHandlerHistory.fromViewAt(0));
assertEquals(newController1.getView(), newController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, newController1.changeHandlerHistory.changeHandlerAt(0).tag);
assertEquals(pushController2Handler.tag, newController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController1.changeHandlerHistory.latestIsPush());
assertNotNull(newController2.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), newController2.changeHandlerHistory.latestFromView());
assertEquals(pushController2Handler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController2.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackForPushHandlers() {
TestController initialController = new TestController();
MockChangeHandler initialPushHandler = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler = MockChangeHandler.taggedHandler("initialPop1", true);
RouterTransaction initialTransaction = RouterTransaction.with(initialController).pushChangeHandler(initialPushHandler).popChangeHandler(initialPopHandler);
router.setRoot(initialTransaction);
View initialView = initialController.getView();
TestController newController = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
initialTransaction,
RouterTransaction.with(newController)
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController.changeHandlerHistory.isValidHistory);
assertTrue(newController.changeHandlerHistory.isValidHistory);
assertEquals(2, initialController.changeHandlerHistory.size());
assertEquals(1, newController.changeHandlerHistory.size());
assertNotNull(initialController.changeHandlerHistory.latestToView());
assertEquals(newController.getView(), initialController.changeHandlerHistory.latestToView());
assertEquals(initialView, initialController.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController.changeHandlerHistory.latestIsPush());
assertTrue(newController.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackForInvertHandlersWithRemovesView() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
RouterTransaction initialTransaction1 = RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1);
router.setRoot(initialTransaction1);
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", true);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", true);
RouterTransaction initialTransaction2 = RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2);
router.pushController(initialTransaction2);
View initialView2 = initialController2.getView();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
initialTransaction2,
initialTransaction1
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertFalse(initialController1.changeHandlerHistory.latestIsPush());
assertNotNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertFalse(initialController2.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackForInvertHandlersWithoutRemovesView() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
RouterTransaction initialTransaction1 = RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1);
router.setRoot(initialTransaction1);
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
RouterTransaction initialTransaction2 = RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2);
router.pushController(initialTransaction2);
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
initialTransaction2,
initialTransaction1
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertEquals(2, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(initialPushHandler2.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertFalse(initialController2.changeHandlerHistory.latestIsPush());
}
}
@@ -0,0 +1,425 @@
package com.bluelinelabs.conductor
import com.bluelinelabs.conductor.util.MockChangeHandler
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.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class RouterChangeHandlerTests {
private val router = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.router
@Test
fun testSetRootHandler() {
val handler = MockChangeHandler.taggedHandler("root", true)
val rootController = TestController()
router.setRoot(
rootController.asTransaction(pushChangeHandler = handler)
)
Assert.assertTrue(rootController.changeHandlerHistory.isValidHistory)
Assert.assertNull(rootController.changeHandlerHistory.latestFromView())
Assert.assertNotNull(rootController.changeHandlerHistory.latestToView())
Assert.assertEquals(
rootController.view,
rootController.changeHandlerHistory.latestToView()
)
Assert.assertTrue(rootController.changeHandlerHistory.latestIsPush())
Assert.assertEquals(handler.tag, rootController.changeHandlerHistory.latestChangeHandler().tag)
}
@Test
fun testPushPopHandlers() {
val rootController = TestController()
router.setRoot(
rootController.asTransaction(pushChangeHandler = MockChangeHandler.defaultHandler())
)
val rootView = rootController.view
val pushHandler = MockChangeHandler.taggedHandler("push", true)
val popHandler = MockChangeHandler.taggedHandler("pop", true)
val pushController = TestController()
router.pushController(
pushController.asTransaction(
pushChangeHandler = pushHandler,
popChangeHandler = popHandler
)
)
Assert.assertTrue(rootController.changeHandlerHistory.isValidHistory)
Assert.assertTrue(pushController.changeHandlerHistory.isValidHistory)
Assert.assertNotNull(pushController.changeHandlerHistory.latestFromView())
Assert.assertNotNull(pushController.changeHandlerHistory.latestToView())
Assert.assertEquals(rootView, pushController.changeHandlerHistory.latestFromView())
Assert.assertEquals(
pushController.view,
pushController.changeHandlerHistory.latestToView()
)
Assert.assertTrue(pushController.changeHandlerHistory.latestIsPush())
Assert.assertEquals(
pushHandler.tag,
pushController.changeHandlerHistory.latestChangeHandler().tag
)
val pushView = pushController.view
router.popController(pushController)
Assert.assertNotNull(pushController.changeHandlerHistory.latestFromView())
Assert.assertNotNull(pushController.changeHandlerHistory.latestToView())
Assert.assertEquals(pushView, pushController.changeHandlerHistory.fromViewAt(1))
Assert.assertEquals(
rootController.view,
pushController.changeHandlerHistory.latestToView()
)
Assert.assertFalse(pushController.changeHandlerHistory.latestIsPush())
Assert.assertEquals(
popHandler.tag,
pushController.changeHandlerHistory.latestChangeHandler().tag
)
}
@Test
fun testResetRootHandlers() {
val initialController1 = TestController()
router.setRoot(
initialController1.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush1", true),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop1", true)
)
)
val initialController2 = TestController()
router.pushController(
initialController2.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush2", false),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop2", false)
)
)
val initialView1 = initialController1.view
val initialView2 = initialController2.view
val newRootController = TestController()
val newRootHandlerTag = "newRootHandler"
router.setRoot(
newRootController.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler(newRootHandlerTag, true)
)
)
Assert.assertTrue(initialController1.changeHandlerHistory.isValidHistory)
Assert.assertTrue(initialController2.changeHandlerHistory.isValidHistory)
Assert.assertTrue(newRootController.changeHandlerHistory.isValidHistory)
Assert.assertEquals(3, initialController1.changeHandlerHistory.size().toLong())
Assert.assertEquals(2, initialController2.changeHandlerHistory.size().toLong())
Assert.assertEquals(1, newRootController.changeHandlerHistory.size().toLong())
Assert.assertNotNull(initialController1.changeHandlerHistory.latestToView())
Assert.assertEquals(
newRootController.view,
initialController1.changeHandlerHistory.latestToView()
)
Assert.assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView())
Assert.assertEquals(
newRootHandlerTag,
initialController1.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController1.changeHandlerHistory.latestIsPush())
Assert.assertNull(initialController2.changeHandlerHistory.latestToView())
Assert.assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView())
Assert.assertEquals(
newRootHandlerTag,
initialController2.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController2.changeHandlerHistory.latestIsPush())
Assert.assertNotNull(newRootController.changeHandlerHistory.latestToView())
Assert.assertEquals(
newRootController.view,
newRootController.changeHandlerHistory.latestToView()
)
Assert.assertEquals(initialView1, newRootController.changeHandlerHistory.latestFromView())
Assert.assertEquals(
newRootHandlerTag,
newRootController.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(newRootController.changeHandlerHistory.latestIsPush())
}
@Test
fun testSetBackstackHandlers() {
val initialController1 = TestController()
router.setRoot(
initialController1.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush1", true),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop1", true)
)
)
val initialController2 = TestController()
router.pushController(
initialController2.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush2", false),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop2", false)
)
)
val initialView1 = initialController1.view
val initialView2 = initialController2.view
val newController1 = TestController()
val newController2 = TestController()
val setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true)
val newBackstack = listOf(newController1.asTransaction(), newController2.asTransaction())
router.setBackstack(newBackstack, setBackstackHandler)
Assert.assertTrue(initialController1.changeHandlerHistory.isValidHistory)
Assert.assertTrue(initialController2.changeHandlerHistory.isValidHistory)
Assert.assertTrue(newController1.changeHandlerHistory.isValidHistory)
Assert.assertEquals(3, initialController1.changeHandlerHistory.size().toLong())
Assert.assertEquals(2, initialController2.changeHandlerHistory.size().toLong())
Assert.assertEquals(0, newController1.changeHandlerHistory.size().toLong())
Assert.assertEquals(1, newController2.changeHandlerHistory.size().toLong())
Assert.assertNotNull(initialController1.changeHandlerHistory.latestToView())
Assert.assertEquals(
newController2.view,
initialController1.changeHandlerHistory.latestToView()
)
Assert.assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController1.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController1.changeHandlerHistory.latestIsPush())
Assert.assertNull(initialController2.changeHandlerHistory.latestToView())
Assert.assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController2.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController2.changeHandlerHistory.latestIsPush())
Assert.assertNotNull(newController2.changeHandlerHistory.latestToView())
Assert.assertEquals(
newController2.view,
newController2.changeHandlerHistory.latestToView()
)
Assert.assertEquals(initialView1, newController2.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
newController2.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(newController2.changeHandlerHistory.latestIsPush())
}
@Test
fun testSetBackstackWithTwoVisibleHandlers() {
val initialController1 = TestController()
router.setRoot(
initialController1.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush1", true),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop1", true)
)
)
val initialController2 = TestController()
router.pushController(
initialController2.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush2", false),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop2", false)
)
)
val initialView1 = initialController1.view
val initialView2 = initialController2.view
val newController1 = TestController()
val newController2 = TestController()
val setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true)
val pushController2Handler = MockChangeHandler.noRemoveViewOnPushHandler("pushController2")
val newBackstack = listOf(
newController1.asTransaction(),
newController2.asTransaction(pushChangeHandler = pushController2Handler)
)
router.setBackstack(newBackstack, setBackstackHandler)
Assert.assertTrue(initialController1.changeHandlerHistory.isValidHistory)
Assert.assertTrue(initialController2.changeHandlerHistory.isValidHistory)
Assert.assertTrue(newController1.changeHandlerHistory.isValidHistory)
Assert.assertEquals(3, initialController1.changeHandlerHistory.size().toLong())
Assert.assertEquals(2, initialController2.changeHandlerHistory.size().toLong())
Assert.assertEquals(2, newController1.changeHandlerHistory.size().toLong())
Assert.assertEquals(1, newController2.changeHandlerHistory.size().toLong())
Assert.assertNotNull(initialController1.changeHandlerHistory.latestToView())
Assert.assertEquals(
newController1.view,
initialController1.changeHandlerHistory.latestToView()
)
Assert.assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController1.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController1.changeHandlerHistory.latestIsPush())
Assert.assertNull(initialController2.changeHandlerHistory.latestToView())
Assert.assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController2.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController2.changeHandlerHistory.latestIsPush())
Assert.assertNotNull(newController1.changeHandlerHistory.latestToView())
Assert.assertEquals(newController1.view, newController1.changeHandlerHistory.toViewAt(0))
Assert.assertEquals(
newController2.view,
newController1.changeHandlerHistory.latestToView()
)
Assert.assertEquals(initialView1, newController1.changeHandlerHistory.fromViewAt(0))
Assert.assertEquals(
newController1.view,
newController1.changeHandlerHistory.latestFromView()
)
Assert.assertEquals(
setBackstackHandler.tag,
newController1.changeHandlerHistory.changeHandlerAt(0).tag
)
Assert.assertEquals(
pushController2Handler.tag,
newController1.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(newController1.changeHandlerHistory.latestIsPush())
Assert.assertNotNull(newController2.changeHandlerHistory.latestToView())
Assert.assertEquals(
newController2.view,
newController2.changeHandlerHistory.latestToView()
)
Assert.assertEquals(
newController1.view,
newController2.changeHandlerHistory.latestFromView()
)
Assert.assertEquals(
pushController2Handler.tag,
newController2.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(newController2.changeHandlerHistory.latestIsPush())
}
@Test
fun testSetBackstackForPushHandlers() {
val initialController = TestController()
val initialTransaction = initialController.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush1", true),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop1", true)
)
router.setRoot(initialTransaction)
val initialView = initialController.view
val newController = TestController()
val setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true)
val newBackstack = listOf(initialTransaction, newController.asTransaction())
router.setBackstack(newBackstack, setBackstackHandler)
Assert.assertTrue(initialController.changeHandlerHistory.isValidHistory)
Assert.assertTrue(newController.changeHandlerHistory.isValidHistory)
Assert.assertEquals(2, initialController.changeHandlerHistory.size().toLong())
Assert.assertEquals(1, newController.changeHandlerHistory.size().toLong())
Assert.assertNotNull(initialController.changeHandlerHistory.latestToView())
Assert.assertEquals(
newController.view,
initialController.changeHandlerHistory.latestToView()
)
Assert.assertEquals(initialView, initialController.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController.changeHandlerHistory.latestIsPush())
Assert.assertTrue(newController.changeHandlerHistory.latestIsPush())
}
@Test
fun testSetBackstackForInvertHandlersWithRemovesView() {
val initialController1 = TestController()
val initialTransaction1 = initialController1.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush1", true),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop1", true)
)
router.setRoot(initialTransaction1)
val initialController2 = TestController()
val initialTransaction2 = initialController2.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush2", true),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop2", true)
)
router.pushController(initialTransaction2)
val initialView2 = initialController2.view
val setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true)
val newBackstack = listOf(initialTransaction2, initialTransaction1)
router.setBackstack(newBackstack, setBackstackHandler)
Assert.assertTrue(initialController1.changeHandlerHistory.isValidHistory)
Assert.assertTrue(initialController2.changeHandlerHistory.isValidHistory)
Assert.assertEquals(3, initialController1.changeHandlerHistory.size().toLong())
Assert.assertEquals(2, initialController2.changeHandlerHistory.size().toLong())
Assert.assertNotNull(initialController1.changeHandlerHistory.latestToView())
Assert.assertEquals(initialView2, initialController1.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController1.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertFalse(initialController1.changeHandlerHistory.latestIsPush())
Assert.assertNotNull(initialController2.changeHandlerHistory.latestToView())
Assert.assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController2.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertFalse(initialController2.changeHandlerHistory.latestIsPush())
}
@Test
fun testSetBackstackForInvertHandlersWithoutRemovesView() {
val initialController1 = TestController()
val initialTransaction1 = initialController1.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler("initialPush1", true),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop1", true)
)
router.setRoot(initialTransaction1)
val initialController2 = TestController()
val initialPushHandler2Tag = "initialPush2"
val initialTransaction2 = initialController2.asTransaction(
pushChangeHandler = MockChangeHandler.taggedHandler(initialPushHandler2Tag, false),
popChangeHandler = MockChangeHandler.taggedHandler("initialPop2", false)
)
router.pushController(initialTransaction2)
val initialView1 = initialController1.view
val initialView2 = initialController2.view
val setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true)
val newBackstack = listOf(initialTransaction2, initialTransaction1)
router.setBackstack(newBackstack, setBackstackHandler)
Assert.assertTrue(initialController1.changeHandlerHistory.isValidHistory)
Assert.assertTrue(initialController2.changeHandlerHistory.isValidHistory)
Assert.assertEquals(2, initialController1.changeHandlerHistory.size().toLong())
Assert.assertEquals(2, initialController2.changeHandlerHistory.size().toLong())
Assert.assertNotNull(initialController1.changeHandlerHistory.latestToView())
Assert.assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView())
Assert.assertEquals(
initialPushHandler2Tag,
initialController1.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertTrue(initialController1.changeHandlerHistory.latestIsPush())
Assert.assertNull(initialController2.changeHandlerHistory.latestToView())
Assert.assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView())
Assert.assertEquals(
setBackstackHandler.tag,
initialController2.changeHandlerHistory.latestChangeHandler().tag
)
Assert.assertFalse(initialController2.changeHandlerHistory.latestIsPush())
}
}
@@ -1,496 +0,0 @@
package com.bluelinelabs.conductor;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RouterTests {
private Router router;
@Before
public void setup() {
ActivityProxy activityProxy = new ActivityProxy().create(null).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), null);
}
@Test
public void testSetRoot() {
String rootTag = "root";
Controller rootController = new TestController();
assertFalse(router.hasRootController());
router.setRoot(RouterTransaction.with(rootController).tag(rootTag));
assertTrue(router.hasRootController());
assertEquals(rootController, router.getControllerWithTag(rootTag));
}
@Test
public void testSetNewRoot() {
String oldRootTag = "oldRoot";
String newRootTag = "newRoot";
Controller oldRootController = new TestController();
Controller newRootController = new TestController();
router.setRoot(RouterTransaction.with(oldRootController).tag(oldRootTag));
router.setRoot(RouterTransaction.with(newRootController).tag(newRootTag));
assertNull(router.getControllerWithTag(oldRootTag));
assertEquals(newRootController, router.getControllerWithTag(newRootTag));
}
@Test
public void testGetByInstanceId() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
assertEquals(controller, router.getControllerWithInstanceId(controller.getInstanceId()));
assertNull(router.getControllerWithInstanceId("fake id"));
}
@Test
public void testGetByTag() {
String controller1Tag = "controller1";
String controller2Tag = "controller2";
Controller controller1 = new TestController();
Controller controller2 = new TestController();
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertEquals(controller2, router.getControllerWithTag(controller2Tag));
}
@Test
public void testPushPopControllers() {
String controller1Tag = "controller1";
String controller2Tag = "controller2";
Controller controller1 = new TestController();
Controller controller2 = new TestController();
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
assertEquals(1, router.getBackstackSize());
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
assertEquals(2, router.getBackstackSize());
router.popCurrentController();
assertEquals(1, router.getBackstackSize());
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
router.popCurrentController();
assertEquals(0, router.getBackstackSize());
assertNull(router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
}
@Test
public void testPopControllerConcurrentModificationException() {
int step = 1;
for (int i = 0; i < 10; i++, step++) {
router.pushController(RouterTransaction.with(new TestController()).tag("1"));
router.pushController(RouterTransaction.with(new TestController()).tag("2"));
router.pushController(RouterTransaction.with(new TestController()).tag("3"));
String tag;
if (step == 1) {
tag = "1";
} else if (step == 2) {
tag = "2";
} else {
tag = "3";
step = 0;
}
Controller controller = router.getControllerWithTag(tag);
if (controller != null) {
router.popController(controller);
}
router.popToRoot();
}
}
@Test
public void testPopToTag() {
String controller1Tag = "controller1";
String controller2Tag = "controller2";
String controller3Tag = "controller3";
String controller4Tag = "controller4";
Controller controller1 = new TestController();
Controller controller2 = new TestController();
Controller controller3 = new TestController();
Controller controller4 = new TestController();
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
router.pushController(RouterTransaction.with(controller3)
.tag(controller3Tag));
router.pushController(RouterTransaction.with(controller4)
.tag(controller4Tag));
router.popToTag(controller2Tag);
assertEquals(2, router.getBackstackSize());
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertEquals(controller2, router.getControllerWithTag(controller2Tag));
assertNull(router.getControllerWithTag(controller3Tag));
assertNull(router.getControllerWithTag(controller4Tag));
}
@Test
public void testPopNonCurrent() {
String controller1Tag = "controller1";
String controller2Tag = "controller2";
String controller3Tag = "controller3";
Controller controller1 = new TestController();
Controller controller2 = new TestController();
Controller controller3 = new TestController();
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
router.pushController(RouterTransaction.with(controller3)
.tag(controller3Tag));
router.popController(controller2);
assertEquals(2, router.getBackstackSize());
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
assertEquals(controller3, router.getControllerWithTag(controller3Tag));
}
@Test
public void testSetBackstack() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
}
@Test
public void testNewSetBackstack() {
router.setRoot(RouterTransaction.with(new TestController()));
assertEquals(1, router.getBackstackSize());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
assertEquals(router, rootTransaction.controller().getRouter());
assertEquals(router, middleTransaction.controller().getRouter());
assertEquals(router, topTransaction.controller().getRouter());
}
@Test
public void testNewSetBackstackWithNoRemoveViewOnPush() {
RouterTransaction oldRootTransaction = RouterTransaction.with(new TestController());
RouterTransaction oldTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
router.setRoot(oldRootTransaction);
router.pushController(oldTopTransaction);
assertEquals(2, router.getBackstackSize());
assertTrue(oldRootTransaction.controller().isAttached());
assertTrue(oldTopTransaction.controller().isAttached());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
assertFalse(oldRootTransaction.controller().isAttached());
assertFalse(oldTopTransaction.controller().isAttached());
assertTrue(rootTransaction.controller().isAttached());
assertTrue(middleTransaction.controller().isAttached());
assertTrue(topTransaction.controller().isAttached());
}
@Test
public void testPopToRoot() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
router.popToRoot();
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller().isAttached());
assertFalse(transaction1.controller().isAttached());
assertFalse(transaction2.controller().isAttached());
}
@Test
public void testPopToRootWithNoRemoveViewOnPush() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
RouterTransaction transaction1 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
RouterTransaction transaction2 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
router.popToRoot();
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller().isAttached());
assertFalse(transaction1.controller().isAttached());
assertFalse(transaction2.controller().isAttached());
}
@Test
public void testReplaceTopController() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(2, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController());
router.replaceTopController(newTopTransaction);
assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(newTopTransaction, fetchedBackstack.get(1));
}
@Test
public void testReplaceTopControllerWithNoRemoveViewOnPush() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(2, router.getBackstackSize());
assertTrue(rootTransaction.controller().isAttached());
assertTrue(topTransaction.controller().isAttached());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
router.replaceTopController(newTopTransaction);
newTopTransaction.pushChangeHandler().completeImmediately();
assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(newTopTransaction, fetchedBackstack.get(1));
assertTrue(rootTransaction.controller().isAttached());
assertFalse(topTransaction.controller().isAttached());
assertTrue(newTopTransaction.controller().isAttached());
}
@Test
public void testRearrangeTransactionBackstack() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(1, transaction1.getTransactionIndex());
assertEquals(2, transaction2.getTransactionIndex());
backstack = Arrays.asList(transaction2, transaction1);
router.setBackstack(backstack, null);
assertEquals(1, transaction2.getTransactionIndex());
assertEquals(2, transaction1.getTransactionIndex());
router.handleBack();
assertEquals(1, router.getBackstackSize());
assertEquals(transaction2, router.getBackstack().get(0));
router.handleBack();
assertEquals(0, router.getBackstackSize());
}
@Test
public void testChildRouterRearrangeTransactionBackstack() {
Controller parent = new TestController();
router.setRoot(RouterTransaction.with(parent));
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction1.getTransactionIndex());
assertEquals(3, transaction2.getTransactionIndex());
backstack = Arrays.asList(transaction2, transaction1);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction2.getTransactionIndex());
assertEquals(3, transaction1.getTransactionIndex());
childRouter.handleBack();
assertEquals(1, childRouter.getBackstackSize());
assertEquals(transaction2, childRouter.getBackstack().get(0));
childRouter.handleBack();
assertEquals(0, childRouter.getBackstackSize());
}
@Test
public void testRemovesAllViewsOnDestroy() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2)
.pushChangeHandler(new FadeChangeHandler(false)));
assertEquals(2, router.container.getChildCount());
router.destroy(true);
assertEquals(0, router.container.getChildCount());
}
@Test
public void testIsBeingDestroyed() {
final LifecycleListener lifecycleListener = new LifecycleListener() {
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
assertTrue(controller.isBeingDestroyed());
}
};
Controller controller1 = new TestController();
Controller controller2 = new TestController();
controller2.addLifecycleListener(lifecycleListener);
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
assertFalse(controller1.isBeingDestroyed());
assertFalse(controller2.isBeingDestroyed());
router.popCurrentController();
assertFalse(controller1.isBeingDestroyed());
assertTrue(controller2.isBeingDestroyed());
Controller controller3 = new TestController();
controller3.addLifecycleListener(lifecycleListener);
router.pushController(RouterTransaction.with(controller3));
assertFalse(controller1.isBeingDestroyed());
assertFalse(controller3.isBeingDestroyed());
router.popToRoot();
assertFalse(controller1.isBeingDestroyed());
assertTrue(controller3.isBeingDestroyed());
}
}
@@ -0,0 +1,436 @@
package com.bluelinelabs.conductor
import android.view.View
import com.bluelinelabs.conductor.Controller.LifecycleListener
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.util.MockChangeHandler
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.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class RouterTests {
private val router = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.router
@Test
fun testSetRoot() {
val rootTag = "root"
val rootController = TestController()
Assert.assertFalse(router.hasRootController())
router.setRoot(RouterTransaction.with(rootController).tag(rootTag))
Assert.assertTrue(router.hasRootController())
Assert.assertEquals(rootController, router.getControllerWithTag(rootTag))
}
@Test
fun testSetNewRoot() {
val oldRootTag = "oldRoot"
val newRootTag = "newRoot"
val oldRootController = TestController()
val newRootController = TestController()
router.setRoot(RouterTransaction.with(oldRootController).tag(oldRootTag))
router.setRoot(RouterTransaction.with(newRootController).tag(newRootTag))
Assert.assertNull(router.getControllerWithTag(oldRootTag))
Assert.assertEquals(newRootController, router.getControllerWithTag(newRootTag))
}
@Test
fun testGetByInstanceId() {
val controller = TestController()
router.pushController(controller.asTransaction())
Assert.assertEquals(
controller,
router.getControllerWithInstanceId(controller.getInstanceId())
)
Assert.assertNull(router.getControllerWithInstanceId("fake id"))
}
@Test
fun testGetByTag() {
val controller1Tag = "controller1"
val controller2Tag = "controller2"
val controller1 = TestController()
val controller2 = TestController()
router.pushController(
RouterTransaction.with(controller1).tag(controller1Tag)
)
router.pushController(
RouterTransaction.with(controller2).tag(controller2Tag)
)
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag))
Assert.assertEquals(controller2, router.getControllerWithTag(controller2Tag))
}
@Test
fun testPushPopControllers() {
val controller1Tag = "controller1"
val controller2Tag = "controller2"
val controller1 = TestController()
val controller2 = TestController()
router.pushController(
RouterTransaction.with(controller1).tag(controller1Tag)
)
Assert.assertEquals(1, router.backstackSize.toLong())
router.pushController(
RouterTransaction.with(controller2).tag(controller2Tag)
)
Assert.assertEquals(2, router.backstackSize.toLong())
router.popCurrentController()
Assert.assertEquals(1, router.backstackSize.toLong())
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag))
Assert.assertNull(router.getControllerWithTag(controller2Tag))
router.popCurrentController()
Assert.assertEquals(0, router.backstackSize.toLong())
Assert.assertNull(router.getControllerWithTag(controller1Tag))
Assert.assertNull(router.getControllerWithTag(controller2Tag))
}
@Test
fun testPopControllerConcurrentModificationException() {
var step = 1
var i = 0
while (i < 10) {
router.pushController(RouterTransaction.with(TestController()).tag("1"))
router.pushController(RouterTransaction.with(TestController()).tag("2"))
router.pushController(RouterTransaction.with(TestController()).tag("3"))
val tag = when (step) {
1 -> "1"
2 -> "2"
else -> {
step = 0
"3"
}
}
val controller = router.getControllerWithTag(tag)
if (controller != null) {
router.popController(controller)
}
router.popToRoot()
i++
step++
}
}
@Test
fun testPopToTag() {
val controller1Tag = "controller1"
val controller2Tag = "controller2"
val controller3Tag = "controller3"
val controller4Tag = "controller4"
val controller1 = TestController()
val controller2 = TestController()
val controller3 = TestController()
val controller4 = TestController()
router.pushController(
RouterTransaction.with(controller1).tag(controller1Tag)
)
router.pushController(
RouterTransaction.with(controller2).tag(controller2Tag)
)
router.pushController(
RouterTransaction.with(controller3).tag(controller3Tag)
)
router.pushController(
RouterTransaction.with(controller4).tag(controller4Tag)
)
router.popToTag(controller2Tag)
Assert.assertEquals(2, router.backstackSize.toLong())
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag))
Assert.assertEquals(controller2, router.getControllerWithTag(controller2Tag))
Assert.assertNull(router.getControllerWithTag(controller3Tag))
Assert.assertNull(router.getControllerWithTag(controller4Tag))
}
@Test
fun testPopNonCurrent() {
val controller1Tag = "controller1"
val controller2Tag = "controller2"
val controller3Tag = "controller3"
val controller1 = TestController()
val controller2 = TestController()
val controller3 = TestController()
router.pushController(
RouterTransaction.with(controller1).tag(controller1Tag)
)
router.pushController(
RouterTransaction.with(controller2).tag(controller2Tag)
)
router.pushController(
RouterTransaction.with(controller3).tag(controller3Tag)
)
router.popController(controller2)
Assert.assertEquals(2, router.backstackSize.toLong())
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag))
Assert.assertNull(router.getControllerWithTag(controller2Tag))
Assert.assertEquals(controller3, router.getControllerWithTag(controller3Tag))
}
@Test
fun testSetBackstack() {
val rootTransaction = TestController().asTransaction()
val middleTransaction = TestController().asTransaction()
val topTransaction = TestController().asTransaction()
val backstack = listOf(rootTransaction, middleTransaction, topTransaction)
router.setBackstack(backstack, null)
Assert.assertEquals(3, router.backstackSize.toLong())
val fetchedBackstack = router.getBackstack()
Assert.assertEquals(rootTransaction, fetchedBackstack[0])
Assert.assertEquals(middleTransaction, fetchedBackstack[1])
Assert.assertEquals(topTransaction, fetchedBackstack[2])
}
@Test
fun testNewSetBackstack() {
router.setRoot(TestController().asTransaction())
Assert.assertEquals(1, router.backstackSize.toLong())
val rootTransaction = TestController().asTransaction()
val middleTransaction = TestController().asTransaction()
val topTransaction = TestController().asTransaction()
val backstack = listOf(rootTransaction, middleTransaction, topTransaction)
router.setBackstack(backstack, null)
Assert.assertEquals(3, router.backstackSize.toLong())
val fetchedBackstack = router.getBackstack()
Assert.assertEquals(rootTransaction, fetchedBackstack[0])
Assert.assertEquals(middleTransaction, fetchedBackstack[1])
Assert.assertEquals(topTransaction, fetchedBackstack[2])
Assert.assertEquals(router, rootTransaction.controller.getRouter())
Assert.assertEquals(router, middleTransaction.controller.getRouter())
Assert.assertEquals(router, topTransaction.controller.getRouter())
}
@Test
fun testNewSetBackstackWithNoRemoveViewOnPush() {
val oldRootTransaction = TestController().asTransaction()
val oldTopTransaction = TestController().asTransaction(
pushChangeHandler = MockChangeHandler.noRemoveViewOnPushHandler()
)
router.setRoot(oldRootTransaction)
router.pushController(oldTopTransaction)
Assert.assertEquals(2, router.backstackSize.toLong())
Assert.assertTrue(oldRootTransaction.controller.isAttached)
Assert.assertTrue(oldTopTransaction.controller.isAttached)
val rootTransaction = TestController().asTransaction()
val middleTransaction = TestController().asTransaction(
pushChangeHandler = MockChangeHandler.noRemoveViewOnPushHandler()
)
val topTransaction = TestController().asTransaction(
pushChangeHandler = MockChangeHandler.noRemoveViewOnPushHandler()
)
val backstack = listOf(rootTransaction, middleTransaction, topTransaction)
router.setBackstack(backstack, null)
Assert.assertEquals(3, router.backstackSize.toLong())
val fetchedBackstack = router.getBackstack()
Assert.assertEquals(rootTransaction, fetchedBackstack[0])
Assert.assertEquals(middleTransaction, fetchedBackstack[1])
Assert.assertEquals(topTransaction, fetchedBackstack[2])
Assert.assertFalse(oldRootTransaction.controller.isAttached)
Assert.assertFalse(oldTopTransaction.controller.isAttached)
Assert.assertTrue(rootTransaction.controller.isAttached)
Assert.assertTrue(middleTransaction.controller.isAttached)
Assert.assertTrue(topTransaction.controller.isAttached)
}
@Test
fun testPopToRoot() {
val rootTransaction = TestController().asTransaction()
val transaction1 = TestController().asTransaction()
val transaction2 = TestController().asTransaction()
val backstack = listOf(rootTransaction, transaction1, transaction2)
router.setBackstack(backstack, null)
Assert.assertEquals(3, router.backstackSize.toLong())
router.popToRoot()
Assert.assertEquals(1, router.backstackSize.toLong())
Assert.assertEquals(rootTransaction, router.getBackstack()[0])
Assert.assertTrue(rootTransaction.controller.isAttached)
Assert.assertFalse(transaction1.controller.isAttached)
Assert.assertFalse(transaction2.controller.isAttached)
}
@Test
fun testPopToRootWithNoRemoveViewOnPush() {
val rootTransaction = TestController().asTransaction(
pushChangeHandler = HorizontalChangeHandler(false)
)
val transaction1 = TestController().asTransaction(
pushChangeHandler = HorizontalChangeHandler(false)
)
val transaction2 = TestController().asTransaction(
pushChangeHandler = HorizontalChangeHandler(false)
)
val backstack = listOf(rootTransaction, transaction1, transaction2)
router.setBackstack(backstack, null)
Assert.assertEquals(3, router.backstackSize.toLong())
router.popToRoot()
Assert.assertEquals(1, router.backstackSize.toLong())
Assert.assertEquals(rootTransaction, router.getBackstack()[0])
Assert.assertTrue(rootTransaction.controller.isAttached)
Assert.assertFalse(transaction1.controller.isAttached)
Assert.assertFalse(transaction2.controller.isAttached)
}
@Test
fun testReplaceTopController() {
val rootTransaction = TestController().asTransaction()
val topTransaction = TestController().asTransaction()
val backstack = listOf(rootTransaction, topTransaction)
router.setBackstack(backstack, null)
Assert.assertEquals(2, router.backstackSize.toLong())
var fetchedBackstack = router.getBackstack()
Assert.assertEquals(rootTransaction, fetchedBackstack[0])
Assert.assertEquals(topTransaction, fetchedBackstack[1])
val newTopTransaction = TestController().asTransaction()
router.replaceTopController(newTopTransaction)
Assert.assertEquals(2, router.backstackSize.toLong())
fetchedBackstack = router.getBackstack()
Assert.assertEquals(rootTransaction, fetchedBackstack[0])
Assert.assertEquals(newTopTransaction, fetchedBackstack[1])
}
@Test
fun testReplaceTopControllerWithNoRemoveViewOnPush() {
val rootTransaction = TestController().asTransaction()
val topTransaction = TestController().asTransaction(
pushChangeHandler = MockChangeHandler.noRemoveViewOnPushHandler()
)
val backstack = listOf(rootTransaction, topTransaction)
router.setBackstack(backstack, null)
Assert.assertEquals(2, router.backstackSize.toLong())
Assert.assertTrue(rootTransaction.controller.isAttached)
Assert.assertTrue(topTransaction.controller.isAttached)
var fetchedBackstack = router.getBackstack()
Assert.assertEquals(rootTransaction, fetchedBackstack[0])
Assert.assertEquals(topTransaction, fetchedBackstack[1])
val newTopTransaction = TestController().asTransaction(
pushChangeHandler = MockChangeHandler.noRemoveViewOnPushHandler()
)
router.replaceTopController(newTopTransaction)
newTopTransaction.pushChangeHandler()!!.completeImmediately()
Assert.assertEquals(2, router.backstackSize.toLong())
fetchedBackstack = router.getBackstack()
Assert.assertEquals(rootTransaction, fetchedBackstack[0])
Assert.assertEquals(newTopTransaction, fetchedBackstack[1])
Assert.assertTrue(rootTransaction.controller.isAttached)
Assert.assertFalse(topTransaction.controller.isAttached)
Assert.assertTrue(newTopTransaction.controller.isAttached)
}
@Test
fun testRearrangeTransactionBackstack() {
val transaction1 = TestController().asTransaction()
val transaction2 = TestController().asTransaction()
var backstack = listOf(transaction1, transaction2)
router.setBackstack(backstack, null)
Assert.assertEquals(1, transaction1.transactionIndex.toLong())
Assert.assertEquals(2, transaction2.transactionIndex.toLong())
backstack = listOf(transaction2, transaction1)
router.setBackstack(backstack, null)
Assert.assertEquals(1, transaction2.transactionIndex.toLong())
Assert.assertEquals(2, transaction1.transactionIndex.toLong())
router.handleBack()
Assert.assertEquals(1, router.backstackSize.toLong())
Assert.assertEquals(transaction2, router.getBackstack()[0])
router.handleBack()
Assert.assertEquals(0, router.backstackSize.toLong())
}
@Test
fun testChildRouterRearrangeTransactionBackstack() {
val parent = TestController()
router.setRoot(parent.asTransaction())
val childRouter = parent.getChildRouter(
parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1)
)
val transaction1 = TestController().asTransaction()
val transaction2 = TestController().asTransaction()
var backstack = listOf(transaction1, transaction2)
childRouter.setBackstack(backstack, null)
Assert.assertEquals(2, transaction1.transactionIndex.toLong())
Assert.assertEquals(3, transaction2.transactionIndex.toLong())
backstack = listOf(transaction2, transaction1)
childRouter.setBackstack(backstack, null)
Assert.assertEquals(2, transaction2.transactionIndex.toLong())
Assert.assertEquals(3, transaction1.transactionIndex.toLong())
childRouter.handleBack()
Assert.assertEquals(1, childRouter.backstackSize.toLong())
Assert.assertEquals(transaction2, childRouter.getBackstack()[0])
childRouter.handleBack()
Assert.assertEquals(0, childRouter.backstackSize.toLong())
}
@Test
fun testRemovesAllViewsOnDestroy() {
router.setRoot(TestController().asTransaction())
router.pushController(
TestController().asTransaction(
pushChangeHandler = FadeChangeHandler(false)
)
)
Assert.assertEquals(2, router.container.childCount.toLong())
router.destroy(true)
Assert.assertEquals(0, router.container.childCount.toLong())
}
@Test
fun testIsBeingDestroyed() {
val lifecycleListener: LifecycleListener = object : LifecycleListener() {
override fun preDestroyView(controller: Controller, view: View) {
Assert.assertTrue(controller.isBeingDestroyed())
}
}
val controller1 = TestController()
val controller2 = TestController()
controller2.addLifecycleListener(lifecycleListener)
router.setRoot(controller1.asTransaction())
router.pushController(controller2.asTransaction())
Assert.assertFalse(controller1.isBeingDestroyed())
Assert.assertFalse(controller2.isBeingDestroyed())
router.popCurrentController()
Assert.assertFalse(controller1.isBeingDestroyed())
Assert.assertTrue(controller2.isBeingDestroyed())
val controller3 = TestController()
controller3.addLifecycleListener(lifecycleListener)
router.pushController(controller3.asTransaction())
Assert.assertFalse(controller1.isBeingDestroyed())
Assert.assertFalse(controller3.isBeingDestroyed())
router.popToRoot()
Assert.assertFalse(controller1.isBeingDestroyed())
Assert.assertTrue(controller3.isBeingDestroyed())
}
}
@@ -1,106 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TargetControllerTests {
private Router router;
public void createActivityController(Bundle savedInstanceState) {
ActivityProxy activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testSiblingTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerB.setTargetController(controllerA);
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNull(controllerA.getTargetController());
assertEquals(controllerA, controllerB.getTargetController());
}
@Test
public void testParentChildTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerB.setTargetController(controllerA);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNull(controllerA.getTargetController());
assertEquals(controllerA, controllerB.getTargetController());
}
@Test
public void testChildParentTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerA.setTargetController(controllerB);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNull(controllerB.getTargetController());
assertEquals(controllerB, controllerA.getTargetController());
}
}
@@ -0,0 +1,98 @@
package com.bluelinelabs.conductor
import com.bluelinelabs.conductor.util.MockChangeHandler
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.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class TargetControllerTests {
private val router = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.router
@Test
fun testSiblingTarget() {
val controllerA = TestController()
val controllerB = TestController()
Assert.assertNull(controllerA.targetController)
Assert.assertNull(controllerB.targetController)
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
controllerB.targetController = controllerA
router.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertNull(controllerA.targetController)
Assert.assertEquals(controllerA, controllerB.targetController)
}
@Test
fun testParentChildTarget() {
val controllerA = TestController()
val controllerB = TestController()
Assert.assertNull(controllerA.targetController)
Assert.assertNull(controllerB.targetController)
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
controllerB.targetController = controllerA
val childRouter = controllerA.getChildRouter(
controllerA.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertNull(controllerA.targetController)
Assert.assertEquals(controllerA, controllerB.targetController)
}
@Test
fun testChildParentTarget() {
val controllerA = TestController()
val controllerB = TestController()
Assert.assertNull(controllerA.targetController)
Assert.assertNull(controllerB.targetController)
router.pushController(
controllerA.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
controllerA.targetController = controllerB
val childRouter = controllerA.getChildRouter(
controllerA.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.pushController(
controllerB.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
popChangeHandler = MockChangeHandler.defaultHandler()
)
)
Assert.assertNull(controllerB.targetController)
Assert.assertEquals(controllerB, controllerA.targetController)
}
}
@@ -1,4 +1,4 @@
package com.bluelinelabs.conductor.util;
package com.bluelinelabs.conductor;
import android.content.Context;
import android.content.Intent;
@@ -14,9 +14,10 @@ import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.util.AttachFakingFrameLayout;
import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.ChangeHandlerHistory;
import com.bluelinelabs.conductor.util.MockChangeHandler;
public class TestController extends Controller {
@@ -1,153 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ViewLeakTests {
private ActivityProxy activityProxy;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testPop() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testPopWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testPopWhenPushNeverCompleted() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler()));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testActivityStop() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
assertNotNull(controller.getView());
activityProxy.stop(true);
assertNull(controller.getView());
}
@Test
public void testActivityStopWhenPushNeverCompleted() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true);
assertNull(controller.getView());
}
@Test
public void testActivityDestroyWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true).destroy();
assertNull(controller.getView());
}
@Test
public void testViewRemovedIfLayeredNotRemovesFromViewOnPush() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
router.pushController(RouterTransaction.with(new TestController()).pushChangeHandler(new SimpleSwapChangeHandler(false)));
View view = controller.view;
assertNotNull(view.getParent());
router.pushController(RouterTransaction.with(new TestController()));
assertNotNull(view.getParent());
router.popToRoot();
assertNull(view.getParent());
}
public static class NeverAddChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable final View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (from != null) {
container.removeView(from);
}
}
}
public static class NeverCompleteChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (from != null) {
container.removeView(from);
}
container.addView(to);
}
}
}
@@ -0,0 +1,143 @@
package com.bluelinelabs.conductor
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
import com.bluelinelabs.conductor.util.TestActivity
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class ViewLeakTests {
private val activityController = Robolectric.buildActivity(TestActivity::class.java).setup()
private val router = activityController.get().router
@Before
fun setup() {
if (!router.hasRootController()) {
router.setRoot(TestController().asTransaction())
}
}
@Test
fun testPop() {
val controller = TestController()
router.pushController(controller.asTransaction())
Assert.assertNotNull(controller.getView())
router.popCurrentController()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNull(controller.getView())
}
@Test
fun testPopWhenPushNeverAdded() {
val controller = TestController()
router.pushController(controller.asTransaction(pushChangeHandler = NeverAddChangeHandler()))
Assert.assertNotNull(controller.getView())
router.popCurrentController()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNull(controller.getView())
}
@Test
fun testPopWhenPushNeverCompleted() {
val controller = TestController()
router.pushController(controller.asTransaction(pushChangeHandler = NeverCompleteChangeHandler()))
Assert.assertNotNull(controller.getView())
router.popCurrentController()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNull(controller.getView())
}
@Test
fun testActivityDestroy() {
val controller = TestController()
router.pushController(controller.asTransaction())
Assert.assertNotNull(controller.getView())
activityController.stop().destroy()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNull(controller.getView())
}
@Test
fun testActivityDestroyWhenPushNeverCompleted() {
val controller = TestController()
router.pushController(controller.asTransaction(pushChangeHandler = NeverCompleteChangeHandler()))
Assert.assertNotNull(controller.getView())
activityController.stop().destroy()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNull(controller.getView())
}
@Test
fun testActivityDestroyWhenPushNeverAdded() {
val controller = TestController()
router.pushController(controller.asTransaction(pushChangeHandler = NeverAddChangeHandler()))
Assert.assertNotNull(controller.getView())
activityController.stop().destroy()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNull(controller.getView())
}
@Test
fun testViewRemovedIfLayeredNotRemovesFromViewOnPush() {
val controller = TestController()
router.pushController(controller.asTransaction())
router.pushController(TestController().asTransaction(pushChangeHandler = SimpleSwapChangeHandler(false)))
val view = controller.view
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNotNull(view.parent)
router.pushController(TestController().asTransaction())
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNotNull(view.parent)
router.popToRoot()
shadowOf(Looper.getMainLooper()).idle()
Assert.assertNull(view.parent)
}
class NeverAddChangeHandler : ControllerChangeHandler() {
override fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener
) {
if (from != null) {
container.removeView(from)
}
}
}
class NeverCompleteChangeHandler : ControllerChangeHandler() {
override fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener
) {
if (from != null) {
container.removeView(from)
}
container.addView(to)
}
}
}
@@ -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)
}
}
}
@@ -1,19 +0,0 @@
package com.bluelinelabs.conductor.util;
import android.app.Activity;
public class TestActivity extends Activity {
public boolean isChangingConfigurations = false;
public boolean isDestroying = false;
@Override
public boolean isChangingConfigurations() {
return isChangingConfigurations;
}
@Override
public boolean isDestroyed() {
return isDestroying || super.isDestroyed();
}
}
@@ -0,0 +1,32 @@
package com.bluelinelabs.conductor.util
import android.app.Activity
import android.os.Bundle
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
class TestActivity : Activity() {
lateinit var router: Router
var changingConfigurations = false
var destroying = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
router = Conductor.attachRouter(
this,
findViewById(android.R.id.content),
savedInstanceState
)
}
override fun isChangingConfigurations(): Boolean {
return changingConfigurations
}
override fun isDestroyed(): Boolean {
return destroying || super.isDestroyed()
}
}
+35 -19
View File
@@ -1,17 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
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,28 +21,50 @@ android {
packagingOptions {
exclude 'META-INF/rxjava.properties'
}
buildFeatures {
viewBinding = true
compose = true
}
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
composeOptions {
kotlinCompilerExtensionVersion libs.versions.compose.get()
}
}
dependencies {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxViewPager2
implementation rootProject.ext.material
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
annotationProcessor rootProject.ext.butterknifeCompiler
implementation rootProject.ext.butterknife
implementation rootProject.ext.picasso
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')
debugImplementation rootProject.ext.leakCanary
releaseImplementation rootProject.ext.leakCanaryNoOp
testImplementation rootProject.ext.leakCanaryNoOp
implementation libs.compose.ui
implementation libs.compose.ui.tooling
implementation libs.compose.foundation
implementation libs.compose.material
implementation libs.activity.compose
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,7 +0,0 @@
package com.bluelinelabs.conductor.demo;
import androidx.appcompat.app.ActionBar;
public interface ActionBarProvider {
ActionBar getSupportActionBar();
}
@@ -1,18 +0,0 @@
package com.bluelinelabs.conductor.demo;
import android.app.Application;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
public class DemoApplication extends Application {
public static RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}
@@ -1,45 +0,0 @@
package com.bluelinelabs.conductor.demo;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Conductor;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.controllers.HomeController;
import butterknife.BindView;
import butterknife.ButterKnife;
public final class MainActivity extends AppCompatActivity implements ActionBarProvider {
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.controller_container) ViewGroup container;
private Router router;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
setSupportActionBar(toolbar);
router = Conductor.attachRouter(this, container, savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new HomeController()));
}
}
@Override
public void onBackPressed() {
if (!router.handleBack()) {
super.onBackPressed();
}
}
}
@@ -0,0 +1,39 @@
package com.bluelinelabs.conductor.demo
import android.os.Bundle
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
class MainActivity : AppCompatActivity(), ToolbarProvider {
private lateinit var binding: ActivityMainBinding
private lateinit var router: Router
override val toolbar: Toolbar
get() = binding.toolbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
router = attachRouter(this, binding.controllerContainer, savedInstanceState)
.setPopRootControllerMode(PopRootControllerMode.NEVER)
if (!router.hasRootController()) {
router.setRoot(with(HomeController()))
}
}
override fun onBackPressed() {
if (!router.handleBack()) {
super.onBackPressed()
}
}
}
@@ -0,0 +1,7 @@
package com.bluelinelabs.conductor.demo
import androidx.appcompat.widget.Toolbar
interface ToolbarProvider {
val toolbar: Toolbar
}
@@ -1,140 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle.Event;
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.ActionBarProvider;
import com.bluelinelabs.conductor.demo.DemoApplication;
import com.bluelinelabs.conductor.demo.R;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
public class ArchLifecycleController extends LifecycleController {
private static final String TAG = "ArchLifecycleController";
@BindView(R.id.tv_title) TextView tvTitle;
private Unbinder unbinder;
private boolean hasExited;
public ArchLifecycleController() {
Log.i(TAG, "Conductor: Constructor called");
getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Event.ON_ANY)
void onLifecycleEvent(@NonNull LifecycleOwner source, @NonNull Event event) {
Log.d(TAG, "Lifecycle: " + source.getClass().getSimpleName() + " emitted event " + event + " and is now in state " + source.getLifecycle().getCurrentState());
}
});
Log.d(TAG, "Lifecycle: " + getClass().getSimpleName() + " is now in state " + getLifecycle().getCurrentState());
}
@Override
protected void onContextAvailable(@NonNull Context context) {
Log.i(TAG, "Conductor: onContextAvailable() called");
super.onContextAvailable(context);
}
@NonNull
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
Log.i(TAG, "Conductor: onCreateView() called");
View view = inflater.inflate(R.layout.controller_lifecycle, container, false);
view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.orange_300));
unbinder = ButterKnife.bind(this, view);
tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));
return view;
}
@Override
protected void onAttach(@NonNull View view) {
Log.i(TAG, "Conductor: onAttach() called");
super.onAttach(view);
(((ActionBarProvider) getActivity()).getSupportActionBar()).setTitle("Arch Components Lifecycle Demo");
}
@Override
protected void onDetach(@NonNull View view) {
Log.i(TAG, "Conductor: onDetach() called");
super.onDetach(view);
}
@Override
protected void onDestroyView(@NonNull View view) {
Log.i(TAG, "Conductor: onDestroyView() called");
super.onDestroyView(view);
unbinder.unbind();
unbinder = null;
}
@Override
protected void onContextUnavailable() {
Log.i(TAG, "Conductor: onContextUnavailable() called");
super.onContextUnavailable();
}
@Override
public void onDestroy() {
Log.i(TAG, "Conductor: onDestroy() called");
super.onDestroy();
if (hasExited) {
DemoApplication.refWatcher.watch(this);
}
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
hasExited = !changeType.isEnter;
if (isDestroyed()) {
DemoApplication.refWatcher.watch(this);
}
}
@OnClick(R.id.btn_next_release_view) void onNextWithReleaseClicked() {
setRetainViewMode(RetainViewMode.RELEASE_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called, followed by the Controller's onDestroyView() and LifecycleObserver's onStop()."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_next_retain_view) void onNextWithRetainClicked() {
setRetainViewMode(RetainViewMode.RETAIN_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
@@ -0,0 +1,110 @@
package com.bluelinelabs.conductor.demo.controllers
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.archlifecycle.LifecycleController
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
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() {
init {
Log.i(TAG, "Conductor: Constructor called")
watchForLeaks()
lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
fun onLifecycleEvent(source: LifecycleOwner, event: Lifecycle.Event) {
Log.d(
TAG,
"Lifecycle: " + source.javaClass.simpleName + " emitted event " + event + " and is now in state " + source.lifecycle.currentState
)
}
})
Log.d(TAG, "Lifecycle: " + javaClass.simpleName + " is now in state " + lifecycle.currentState)
}
override fun onContextAvailable(context: Context) {
Log.i(TAG, "Conductor: onContextAvailable() called")
super.onContextAvailable(context)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
Log.i(TAG, "Conductor: onCreateView() called")
val binding = ControllerLifecycleBinding.inflate(inflater, container, false)
binding.root.setBackgroundColor(ContextCompat.getColor(container.context, R.color.orange_300))
binding.title.text = binding.root.resources.getString(R.string.rxlifecycle_title, TAG)
binding.nextReleaseView.setOnClickListener {
retainViewMode = RetainViewMode.RELEASE_DETACH
router.pushController(
RouterTransaction.with(TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called, followed by the Controller's onDestroyView() and LifecycleObserver's onStop()."))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
binding.nextRetainView.setOnClickListener {
retainViewMode = RetainViewMode.RETAIN_DETACH
router.pushController(
RouterTransaction.with(TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called."))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
return binding.root
}
override fun onAttach(view: View) {
Log.i(TAG, "Conductor: onAttach() called")
super.onAttach(view)
(activity as ToolbarProvider).toolbar.title = "Arch Components Lifecycle Demo"
}
override fun onDetach(view: View) {
Log.i(TAG, "Conductor: onDetach() called")
super.onDetach(view)
}
override fun onDestroyView(view: View) {
Log.i(TAG, "Conductor: onDestroyView() called")
super.onDestroyView(view)
}
override fun onContextUnavailable() {
Log.i(TAG, "Conductor: onContextUnavailable() called")
super.onContextUnavailable()
}
override fun onDestroy() {
Log.i(TAG, "Conductor: onDestroy() called")
}
companion object {
private const val TAG = "ArchLifecycleController"
}
}
@@ -1,143 +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 android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.ActionBarProvider;
import com.bluelinelabs.conductor.demo.DemoApplication;
import com.bluelinelabs.conductor.demo.R;
import com.uber.autodispose.AutoDispose;
import com.uber.autodispose.lifecycle.LifecycleScopeProvider;
import java.util.concurrent.TimeUnit;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import io.reactivex.Observable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
// Shamelessly borrowed from the official RxLifecycle demo by Trello and adapted for Conductor Controllers
// instead of Activities or Fragments.
public class AutodisposeController extends Controller {
private static final String TAG = "AutodisposeController";
@BindView(R.id.tv_title) TextView tvTitle;
private Unbinder unbinder;
private boolean hasExited;
private final LifecycleScopeProvider scopeProvider = ControllerScopeProvider.from(this);
public AutodisposeController() {
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(() -> Log.i(TAG, "Disposing from constructor"))
.as(AutoDispose.<Long>autoDisposable((scopeProvider)))
.subscribe(num -> Log.i(TAG, "Started in constructor, running until onDestroy(): " + num));
}
@NonNull
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
Log.i(TAG, "onCreateView() called");
View view = inflater.inflate(R.layout.controller_lifecycle, container, false);
view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.purple_300));
unbinder = ButterKnife.bind(this, view);
tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(() -> Log.i(TAG, "Disposing from onCreateView()"))
.as(AutoDispose.<Long>autoDisposable((scopeProvider)))
.subscribe(num -> Log.i(TAG, "Started in onCreateView(), running until onDestroyView(): " + num));
return view;
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
Log.i(TAG, "onAttach() called");
(((ActionBarProvider) getActivity()).getSupportActionBar()).setTitle("Autodispose Demo");
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(() -> Log.i(TAG, "Disposing from onAttach()"))
.as(AutoDispose.<Long>autoDisposable((scopeProvider)))
.subscribe(num -> Log.i(TAG, "Started in onAttach(), running until onDetach(): " + num));
}
@Override
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
Log.i(TAG, "onDestroyView() called");
unbinder.unbind();
unbinder = null;
}
@Override
protected void onDetach(@NonNull View view) {
super.onDetach(view);
Log.i(TAG, "onDetach() called");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy() called");
if (hasExited) {
DemoApplication.refWatcher.watch(this);
}
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
hasExited = !changeType.isEnter;
if (isDestroyed()) {
DemoApplication.refWatcher.watch(this);
}
}
@OnClick(R.id.btn_next_release_view)
void onNextWithReleaseClicked() {
setRetainViewMode(RetainViewMode.RELEASE_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the observables from onAttach() and onViewBound() have been disposed of, while the constructor observable is still running."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_next_retain_view)
void onNextWithRetainClicked() {
setRetainViewMode(RetainViewMode.RETAIN_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the observables from onAttach() has been disposed of, while the constructor and onViewBound() observables are still running."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
@@ -0,0 +1,104 @@
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.Controller
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
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
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 AutodisposeController : Controller() {
private val scopeProvider = ControllerScopeProvider.from(this)
init {
watchForLeaks()
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from constructor") }
.autoDisposable(scopeProvider)
.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()") }
.autoDisposable(scopeProvider)
.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 = "Autodispose Demo"
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from onAttach()") }
.autoDisposable(scopeProvider)
.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")
}
companion object {
private const val TAG = "AutodisposeController"
}
}
@@ -1,56 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.BindView;
public class ChildController extends BaseController {
private static final String KEY_TITLE = "ChildController.title";
private static final String KEY_BG_COLOR = "ChildController.bgColor";
private static final String KEY_COLOR_IS_RES = "ChildController.colorIsResId";
@BindView(R.id.tv_title) TextView tvTitle;
public ChildController(String title, int backgroundColor, boolean colorIsResId) {
this(new BundleBuilder(new Bundle())
.putString(KEY_TITLE, title)
.putInt(KEY_BG_COLOR, backgroundColor)
.putBoolean(KEY_COLOR_IS_RES, colorIsResId)
.build());
}
public ChildController(Bundle args) {
super(args);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_child, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
tvTitle.setText(getArgs().getString(KEY_TITLE));
int bgColor = getArgs().getInt(KEY_BG_COLOR);
if (getArgs().getBoolean(KEY_COLOR_IS_RES)) {
bgColor = ContextCompat.getColor(getActivity(), bgColor);
}
view.setBackgroundColor(bgColor);
}
}
@@ -0,0 +1,39 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerChildBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class ChildController(args: Bundle) : BaseController(R.layout.controller_child, args) {
private val binding: ControllerChildBinding by viewBinding(ControllerChildBinding::bind)
constructor(title: String, backgroundColor: Int, colorIsResId: Boolean) : this(
bundleOf(
KEY_TITLE to title,
KEY_BG_COLOR to backgroundColor,
KEY_COLOR_IS_RES to colorIsResId
)
)
override fun onViewCreated(view: View) {
binding.title.text = args.getString(KEY_TITLE)
val bgColor = args.getInt(KEY_BG_COLOR)
if (args.getBoolean(KEY_COLOR_IS_RES)) {
view.setBackgroundColor(ContextCompat.getColor(view.context, bgColor))
} else {
view.setBackgroundColor(bgColor)
}
}
companion object {
private const val KEY_TITLE = "ChildController.title"
private const val KEY_BG_COLOR = "ChildController.bgColor"
private const val KEY_COLOR_IS_RES = "ChildController.colorIsResId"
}
}
@@ -1,167 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.BindView;
import butterknife.ButterKnife;
public class CityDetailController extends BaseController {
private static final String KEY_TITLE = "CityDetailController.title";
private static final String KEY_IMAGE = "CityDetailController.image";
private static final String[] LIST_ROWS = new String[] {
"• This is a city.",
"• There's some cool stuff about it.",
"• But really this is just a demo, not a city guide app.",
"• This demo is meant to show some nice transitions.",
"• You should have seen some sweet shared element transitions using the ImageView and the TextView in the \"header\" above.",
"• This transition utilized some callbacks to ensure all the necessary rows in the RecyclerView were laid about before the transition occurred.",
"• Just adding some more lines so it scrolls now...\n\n\n\n\n\n\nThe end."
};
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@DrawableRes private int imageDrawableRes;
private String title;
public CityDetailController(@DrawableRes int imageDrawableRes, String title) {
this(new BundleBuilder(new Bundle())
.putInt(KEY_IMAGE, imageDrawableRes)
.putString(KEY_TITLE, title)
.build());
}
public CityDetailController(Bundle args) {
super(args);
imageDrawableRes = getArgs().getInt(KEY_IMAGE);
title = getArgs().getString(KEY_TITLE);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_city_detail, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new CityDetailAdapter(LayoutInflater.from(view.getContext()), title, imageDrawableRes, LIST_ROWS, title));
}
@Override
protected String getTitle() {
return title;
}
static class CityDetailAdapter extends RecyclerView.Adapter<CityDetailAdapter.ViewHolder> {
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_DETAIL = 1;
private final LayoutInflater inflater;
private final String title;
@DrawableRes private final int imageDrawableRes;
private final String imageViewTransitionName;
private final String textViewTransitionName;
private final String[] details;
public CityDetailAdapter(LayoutInflater inflater, String title, @DrawableRes int imageDrawableRes, String[] details, String transitionNameBase) {
this.inflater = inflater;
this.title = title;
this.imageDrawableRes = imageDrawableRes;
this.details = details;
imageViewTransitionName = inflater.getContext().getResources().getString(R.string.transition_tag_image_named, transitionNameBase);
textViewTransitionName = inflater.getContext().getResources().getString(R.string.transition_tag_title_named, transitionNameBase);
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return VIEW_TYPE_HEADER;
} else {
return VIEW_TYPE_DETAIL;
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_HEADER) {
return new HeaderViewHolder(inflater.inflate(R.layout.row_city_header, parent, false));
} else {
return new DetailViewHolder(inflater.inflate(R.layout.row_city_detail, parent, false));
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (getItemViewType(position) == VIEW_TYPE_HEADER) {
((HeaderViewHolder)holder).bind(imageDrawableRes, title, imageViewTransitionName, textViewTransitionName);
} else {
((DetailViewHolder)holder).bind(details[position - 1]);
}
}
@Override
public int getItemCount() {
return 1 + details.length;
}
static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
static class HeaderViewHolder extends ViewHolder {
@BindView(R.id.image_view) ImageView imageView;
@BindView(R.id.text_view) TextView textView;
public HeaderViewHolder(View itemView) {
super(itemView);
}
void bind(@DrawableRes int imageDrawableRes, String title, String imageTransitionName, String textViewTransitionName) {
imageView.setImageResource(imageDrawableRes);
textView.setText(title);
ViewCompat.setTransitionName(imageView, imageTransitionName);
ViewCompat.setTransitionName(textView, textViewTransitionName);
}
}
static class DetailViewHolder extends ViewHolder {
@BindView(R.id.text_view) TextView textView;
public DetailViewHolder(View itemView) {
super(itemView);
}
void bind(String detail) {
textView.setText(detail);
}
}
}
}
@@ -0,0 +1,121 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerCityDetailBinding
import com.bluelinelabs.conductor.demo.databinding.RowCityDetailBinding
import com.bluelinelabs.conductor.demo.databinding.RowCityHeaderBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class CityDetailController(args: Bundle) : BaseController(R.layout.controller_city_detail, args) {
private val binding: ControllerCityDetailBinding by viewBinding(ControllerCityDetailBinding::bind)
override val title = args.getString(KEY_TITLE)!!
constructor(@DrawableRes image: Int, title: String) : this(
bundleOf(
KEY_TITLE to title,
KEY_IMAGE to image
)
)
override fun onViewCreated(view: View) {
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
binding.recyclerView.adapter = CityDetailAdapter(
inflater = LayoutInflater.from(view.context),
title = title,
imageDrawableRes = args.getInt(KEY_IMAGE),
details = LIST_ROWS,
transitionNameBase = title
)
}
companion object {
private const val KEY_TITLE = "CityDetailController.title"
private const val KEY_IMAGE = "CityDetailController.image"
private val LIST_ROWS = listOf(
"• This is a city.",
"• There's some cool stuff about it.",
"• But really this is just a demo, not a city guide app.",
"• This demo is meant to show some nice transitions.",
"• You should have seen some sweet shared element transitions using the ImageView and the TextView in the \"header\" above.",
"• This transition utilized some callbacks to ensure all the necessary rows in the RecyclerView were laid about before the transition occurred.",
"• Just adding some more lines so it scrolls now...\n\n\n\n\n\n\nThe end."
)
}
class CityDetailAdapter(
private val inflater: LayoutInflater,
private val title: String,
@DrawableRes private val imageDrawableRes: Int,
private val details: List<String>,
private val transitionNameBase: String
) : RecyclerView.Adapter<ViewHolder<*>>() {
override fun getItemViewType(position: Int): Int {
return if (position == 0) VIEW_TYPE_HEADER else VIEW_TYPE_DETAIL
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
return if (viewType == VIEW_TYPE_HEADER) {
HeaderViewHolder(RowCityHeaderBinding.inflate(inflater, parent, false))
} else {
DetailViewHolder(RowCityDetailBinding.inflate(inflater, parent, false))
}
}
override fun onBindViewHolder(holder: ViewHolder<*>, position: Int) {
when (holder) {
is HeaderViewHolder -> {
holder.bind(
imageDrawableRes = imageDrawableRes,
title = title,
imageTransitionName = inflater.context.resources.getString(R.string.transition_tag_image_named, transitionNameBase),
textViewTransitionName = inflater.context.resources.getString(R.string.transition_tag_title_named, transitionNameBase)
)
}
is DetailViewHolder -> {
holder.bind(details[position - 1])
}
else -> {
throw IllegalStateException("Invalid view holder class ${holder.javaClass.canonicalName}")
}
}
}
override fun getItemCount() = details.size + 1
companion object {
private const val VIEW_TYPE_HEADER = 0
private const val VIEW_TYPE_DETAIL = 1
}
}
open class ViewHolder<Binding : ViewBinding>(binding: Binding) : RecyclerView.ViewHolder(binding.root)
class HeaderViewHolder(private val binding: RowCityHeaderBinding) : ViewHolder<RowCityHeaderBinding>(binding) {
fun bind(@DrawableRes imageDrawableRes: Int, title: String?, imageTransitionName: String?, textViewTransitionName: String?) {
binding.imageView.setImageResource(imageDrawableRes)
binding.textView.text = title
binding.imageView.transitionName = imageTransitionName
binding.textView.transitionName = textViewTransitionName
}
}
class DetailViewHolder(private val binding: RowCityDetailBinding) : ViewHolder<RowCityDetailBinding>(binding) {
fun bind(detail: String) {
binding.textView.text = detail
}
}
}
@@ -1,168 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.graphics.PorterDuff.Mode;
import android.os.Bundle;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.CityGridSharedElementTransitionChangeHandler;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class CityGridController extends BaseController {
private static final String KEY_TITLE = "CityGridController.title";
private static final String KEY_DOT_COLOR = "CityGridController.dotColor";
private static final String KEY_FROM_POSITION = "CityGridController.position";
private static final CityModel[] CITY_MODELS = new CityModel[] {
new CityModel(R.drawable.chicago, "Chicago"),
new CityModel(R.drawable.jakarta, "Jakarta"),
new CityModel(R.drawable.london, "London"),
new CityModel(R.drawable.sao_paulo, "Sao Paulo"),
new CityModel(R.drawable.tokyo, "Tokyo")
};
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.img_dot) ImageView imgDot;
@BindView(R.id.recycler_view) RecyclerView recyclerView;
private String title;
private int dotColor;
private int fromPosition;
public CityGridController(String title, int dotColor, int fromPosition) {
this(new BundleBuilder(new Bundle())
.putString(KEY_TITLE, title)
.putInt(KEY_DOT_COLOR, dotColor)
.putInt(KEY_FROM_POSITION, fromPosition)
.build());
}
public CityGridController(Bundle args) {
super(args);
title = getArgs().getString(KEY_TITLE);
dotColor = getArgs().getInt(KEY_DOT_COLOR);
fromPosition = getArgs().getInt(KEY_FROM_POSITION);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_city_grid, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
tvTitle.setText(title);
imgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), dotColor), Mode.SRC_ATOP);
ViewCompat.setTransitionName(tvTitle, getResources().getString(R.string.transition_tag_title_indexed, fromPosition));
ViewCompat.setTransitionName(imgDot, getResources().getString(R.string.transition_tag_dot_indexed, fromPosition));
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new GridLayoutManager(view.getContext(), 2));
recyclerView.setAdapter(new CityGridAdapter(LayoutInflater.from(view.getContext()), CITY_MODELS));
}
@Override
protected String getTitle() {
return "Shared Element Demos";
}
void onModelRowClick(CityModel model) {
String imageTransitionName = getResources().getString(R.string.transition_tag_image_named, model.title);
String titleTransitionName = getResources().getString(R.string.transition_tag_title_named, model.title);
List<String> names = new ArrayList<>();
names.add(imageTransitionName);
names.add(titleTransitionName);
getRouter().pushController(RouterTransaction.with(new CityDetailController(model.drawableRes, model.title))
.pushChangeHandler(new CityGridSharedElementTransitionChangeHandler(names))
.popChangeHandler(new CityGridSharedElementTransitionChangeHandler(names)));
}
class CityGridAdapter extends RecyclerView.Adapter<CityGridAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final CityModel[] items;
public CityGridAdapter(LayoutInflater inflater, CityModel[] items) {
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.row_city_grid, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items[position]);
}
@Override
public int getItemCount() {
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_title) TextView textView;
@BindView(R.id.img_city) ImageView imageView;
private CityModel model;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(CityModel item) {
model = item;
imageView.setImageResource(item.drawableRes);
textView.setText(item.title);
ViewCompat.setTransitionName(textView, getResources().getString(R.string.transition_tag_title_named, model.title));
ViewCompat.setTransitionName(imageView, getResources().getString(R.string.transition_tag_image_named, model.title));
}
@OnClick(R.id.row_root)
void onRowClick() {
onModelRowClick(model);
}
}
}
private static class CityModel {
@DrawableRes int drawableRes;
String title;
public CityModel(@DrawableRes int drawableRes, String title) {
this.drawableRes = drawableRes;
this.title = title;
}
}
}
@@ -0,0 +1,112 @@
package com.bluelinelabs.conductor.demo.controllers
import android.graphics.PorterDuff
import android.os.Bundle
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
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.changehandler.CityGridSharedElementTransitionChangeHandler
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerCityGridBinding
import com.bluelinelabs.conductor.demo.databinding.RowCityGridBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class CityGridController(args: Bundle) : BaseController(R.layout.controller_city_grid, args) {
private val binding: ControllerCityGridBinding by viewBinding(ControllerCityGridBinding::bind)
override val title = "Shared Element Demos"
constructor(title: String?, dotColor: Int, fromPosition: Int) : this(
bundleOf(
KEY_TITLE to title,
KEY_DOT_COLOR to dotColor,
KEY_FROM_POSITION to fromPosition
)
)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.title.text = args.getString(KEY_TITLE)!!
binding.dot.drawable.setColorFilter(ContextCompat.getColor(view.context, args.getInt(KEY_DOT_COLOR)), PorterDuff.Mode.SRC_ATOP)
binding.title.transitionName = view.resources.getString(R.string.transition_tag_title_indexed, args.getInt(KEY_FROM_POSITION))
binding.dot.transitionName = view.resources.getString(R.string.transition_tag_dot_indexed, args.getInt(KEY_FROM_POSITION))
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = GridLayoutManager(view.context, 2)
binding.recyclerView.adapter = CityGridAdapter(LayoutInflater.from(view.context), CITY_MODELS, ::onModelRowClick)
}
private fun onModelRowClick(model: CityModel) {
val names = listOf(
resources!!.getString(R.string.transition_tag_title_named, model.title),
resources!!.getString(R.string.transition_tag_image_named, model.title)
)
router.pushController(
RouterTransaction.with(CityDetailController(model.drawableRes, model.title))
.pushChangeHandler(CityGridSharedElementTransitionChangeHandler(names))
.popChangeHandler(CityGridSharedElementTransitionChangeHandler(names))
)
}
companion object {
private const val KEY_TITLE = "CityGridController.title"
private const val KEY_DOT_COLOR = "CityGridController.dotColor"
private const val KEY_FROM_POSITION = "CityGridController.position"
private val CITY_MODELS = arrayOf(
CityModel(R.drawable.chicago, "Chicago"),
CityModel(R.drawable.jakarta, "Jakarta"),
CityModel(R.drawable.london, "London"),
CityModel(R.drawable.sao_paulo, "Sao Paulo"),
CityModel(R.drawable.tokyo, "Tokyo")
)
}
}
data class CityModel(@DrawableRes val drawableRes: Int, val title: String)
private class CityGridAdapter(
private val inflater: LayoutInflater,
private val items: Array<CityModel>,
private val modelClickListener: (CityModel) -> Unit
) : RecyclerView.Adapter<CityGridAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
RowCityGridBinding.inflate(inflater, parent, false),
modelClickListener
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
class ViewHolder(
private val binding: RowCityGridBinding,
private val modelClickListener: (CityModel) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: CityModel) {
binding.image.setImageResource(item.drawableRes)
binding.title.text = item.title
binding.image.transitionName = itemView.resources.getString(R.string.transition_tag_image_named, item.title)
binding.image.transitionName = itemView.resources.getString(R.string.transition_tag_title_named, item.title)
itemView.setOnClickListener { modelClickListener(item) }
}
}
}
@@ -0,0 +1,85 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.asTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.ToolbarProvider
class ComposeController : Controller() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
return ComposeView(container.context).apply {
setContent {
LongNumberList {
router.pushController(
ComposeController().asTransaction(
pushChangeHandler = HorizontalChangeHandler(),
popChangeHandler = HorizontalChangeHandler()
)
)
}
}
}
}
override fun onAttach(view: View) {
super.onAttach(view)
(activity as? ToolbarProvider)?.toolbar?.apply {
title = "Jetpack Compose Demo"
menu.clear()
}
}
}
@Composable
fun LongNumberList(onClick: () -> Unit) {
MaterialTheme {
LazyColumn {
item {
Spacer(modifier = Modifier.height(8.dp))
}
items((0..100).toList()) {
Box(
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp)
) {
Text(
modifier = Modifier
.align(Alignment.CenterStart),
text = "Line $it"
)
}
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@@ -1,60 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.BindView;
import butterknife.OnClick;
public class DialogController extends BaseController {
private static final String KEY_TITLE = "DialogController.title";
private static final String KEY_DESCRIPTION = "DialogController.description";
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.tv_description) TextView tvDescription;
public DialogController(CharSequence title, CharSequence description) {
this(new BundleBuilder(new Bundle())
.putCharSequence(KEY_TITLE, title)
.putCharSequence(KEY_DESCRIPTION, description)
.build());
}
public DialogController(Bundle args) {
super(args);
}
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_dialog, container, false);
}
@Override
public void onViewBound(@NonNull View view) {
super.onViewBound(view);
tvTitle.setText(getArgs().getCharSequence(KEY_TITLE));
tvDescription.setText(getArgs().getCharSequence(KEY_DESCRIPTION));
tvDescription.setMovementMethod(LinkMovementMethod.getInstance());
}
@OnClick({R.id.dismiss, R.id.dialog_window})
public void dismissDialog() {
getRouter().popController(this);
}
@Override
public boolean handleBack() {
return super.handleBack();
}
}
@@ -0,0 +1,35 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerDialogBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class DialogController(args: Bundle) : BaseController(R.layout.controller_dialog, args) {
private val binding: ControllerDialogBinding by viewBinding(ControllerDialogBinding::bind)
constructor(title: CharSequence, description: CharSequence) : this(
bundleOf(
KEY_TITLE to title,
KEY_DESCRIPTION to description
)
)
override fun onViewCreated(view: View) {
binding.title.text = args.getCharSequence(KEY_TITLE)
binding.description.text = args.getCharSequence(KEY_DESCRIPTION)
binding.description.movementMethod = LinkMovementMethod.getInstance()
binding.dismiss.setOnClickListener { router.popController(this) }
binding.dialogBackground.setOnClickListener { router.popController(this) }
}
companion object {
private const val KEY_TITLE = "DialogController.title"
private const val KEY_DESCRIPTION = "DialogController.description"
}
}
@@ -1,49 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.ScaleFadeChangeHandler;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout;
import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout.ElasticDragDismissCallback;
@TargetApi(VERSION_CODES.LOLLIPOP)
public class DragDismissController extends BaseController {
private final ElasticDragDismissCallback dragDismissListener = new ElasticDragDismissCallback() {
@Override
public void onDragDismissed() {
overridePopHandler(new ScaleFadeChangeHandler());
getRouter().popController(DragDismissController.this);
}
};
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_drag_dismiss, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
((ElasticDragDismissFrameLayout)view).addListener(dragDismissListener);
}
@Override
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
((ElasticDragDismissFrameLayout)view).removeListener(dragDismissListener);
}
@Override
protected String getTitle() {
return "Drag to Dismiss";
}
}
@@ -0,0 +1,28 @@
package com.bluelinelabs.conductor.demo.controllers
import android.view.View
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.changehandler.ScaleFadeChangeHandler
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout
import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout.ElasticDragDismissCallback
class DragDismissController : BaseController(R.layout.controller_drag_dismiss) {
override val title = "Drag to Dismiss"
private val dragDismissListener: ElasticDragDismissCallback = object : ElasticDragDismissCallback() {
override fun onDragDismissed() {
overridePopHandler(ScaleFadeChangeHandler())
router.popController(this@DragDismissController)
}
}
override fun onViewCreated(view: View) {
(view as ElasticDragDismissFrameLayout).addListener(dragDismissListener)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
(view as ElasticDragDismissFrameLayout).removeListener(dragDismissListener)
}
}
@@ -1,147 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.graphics.PorterDuff.Mode;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class ExternalModulesController extends BaseController {
private enum DemoModel {
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);
String title;
@ColorRes int color;
DemoModel(String title, @ColorRes int color) {
this.title = title;
this.color = color;
}
}
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_additional_modules, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new AdditionalModulesAdapter(LayoutInflater.from(view.getContext()), DemoModel.values()));
}
@Override
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType.isEnter) {
setTitle();
}
}
@Override
protected String getTitle() {
return "External Module Demos";
}
void onModelRowClick(DemoModel model) {
switch (model) {
case AUTODISPOSE:
getRouter().pushController(RouterTransaction.with(new AutodisposeController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case RX_LIFECYCLE_2:
getRouter().pushController(RouterTransaction.with(new RxLifecycle2Controller())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case ARCH_LIFECYCLE:
getRouter().pushController(RouterTransaction.with(new ArchLifecycleController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
}
}
class AdditionalModulesAdapter extends RecyclerView.Adapter<AdditionalModulesAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final DemoModel[] items;
public AdditionalModulesAdapter(LayoutInflater inflater, DemoModel[] items) {
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.row_home, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items[position]);
}
@Override
public int getItemCount() {
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.img_dot) ImageView imgDot;
private DemoModel model;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(DemoModel item) {
model = item;
tvTitle.setText(item.title);
imgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), item.color), Mode.SRC_ATOP);
}
@OnClick(R.id.row_root)
void onRowClick() {
onModelRowClick(model);
}
}
}
}
@@ -0,0 +1,89 @@
package com.bluelinelabs.conductor.demo.controllers
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerAdditionalModulesBinding
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
)
override val title = "External Module Demos"
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
binding.recyclerView.adapter = AdditionalModulesAdapter(
LayoutInflater.from(view.context),
ModuleModel.values(),
::onModelRowClick
)
}
private fun onModelRowClick(model: ModuleModel) {
when (model) {
ModuleModel.AUTODISPOSE -> router.pushController(
with(AutodisposeController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
ModuleModel.ARCH_LIFECYCLE -> router.pushController(
with(ArchLifecycleController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
}
}
private enum class ModuleModel(val title: String, @ColorRes val color: Int) {
AUTODISPOSE("Autodispose", R.color.purple_300),
ARCH_LIFECYCLE("Arch Components Lifecycle", R.color.orange_300);
}
private class AdditionalModulesAdapter(
private val inflater: LayoutInflater,
private val items: Array<ModuleModel>,
private val modelClickListener: (ModuleModel) -> Unit
) : RecyclerView.Adapter<AdditionalModulesAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(RowHomeBinding.inflate(inflater, parent, false), modelClickListener)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int {
return items.size
}
class ViewHolder(
private val binding: RowHomeBinding,
private val modelClickListener: (ModuleModel) -> Unit
) : 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
)
itemView.setOnClickListener { modelClickListener(item) }
}
}
}
@@ -1,286 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.content.Intent;
import android.graphics.PorterDuff.Mode;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.ArcFadeMoveChangeHandler;
import com.bluelinelabs.conductor.demo.changehandler.FabToDialogTransitionChangeHandler;
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class HomeController extends BaseController {
private enum DemoModel {
NAVIGATION("Navigation Demos", R.color.red_300),
TRANSITIONS("Transition Demos", R.color.blue_grey_300),
SHARED_ELEMENT_TRANSITIONS("Shared Element Demos", R.color.purple_300),
CHILD_CONTROLLERS("Child Controllers", R.color.orange_300),
VIEW_PAGER("ViewPager", R.color.green_300),
VIEW_PAGER_2("ViewPager2", R.color.pink_300),
TARGET_CONTROLLER("Target Controller", R.color.deep_orange_300),
MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.grey_300),
MASTER_DETAIL("Master Detail", R.color.lime_300),
DRAG_DISMISS("Drag Dismiss", R.color.teal_300),
EXTERNAL_MODULES("Bonus Modules", R.color.deep_purple_300);
String title;
@ColorRes int color;
DemoModel(String title, @ColorRes int color) {
this.title = title;
this.color = color;
}
}
private static final String KEY_FAB_VISIBILITY = "HomeController.fabVisibility";
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@BindView(R.id.fab) View fab;
public HomeController() {
setHasOptionsMenu(true);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_home, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new HomeAdapter(LayoutInflater.from(view.getContext()), DemoModel.values()));
}
@Override
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
super.onSaveViewState(view, outState);
outState.putInt(KEY_FAB_VISIBILITY, fab.getVisibility());
}
@Override
protected void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) {
super.onRestoreViewState(view, savedViewState);
//noinspection WrongConstant
fab.setVisibility(savedViewState.getInt(KEY_FAB_VISIBILITY));
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.home, menu);
}
@Override
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
setOptionsMenuHidden(!changeType.isEnter);
if (changeType.isEnter) {
setTitle();
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.about) {
onFabClicked(false);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected String getTitle() {
return "Conductor Demos";
}
@OnClick(R.id.fab)
public void onFabClicked() {
onFabClicked(true);
}
private void onFabClicked(boolean fromFab) {
SpannableString details = new SpannableString("A small, yet full-featured framework that allows building View-based Android applications");
details.setSpan(new AbsoluteSizeSpan(16, true), 0, details.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
final String url = "https://github.com/bluelinelabs/Conductor";
SpannableString link = new SpannableString(url);
link.setSpan(new URLSpan(url) {
@Override
public void onClick(View widget) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}, 0, link.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
SpannableStringBuilder description = new SpannableStringBuilder();
description.append(details);
description.append("\n\n");
description.append(link);
ControllerChangeHandler pushHandler = fromFab ? new FabToDialogTransitionChangeHandler() : new FadeChangeHandler(false);
ControllerChangeHandler popHandler = fromFab ? new FabToDialogTransitionChangeHandler() : new FadeChangeHandler();
getRouter()
.pushController(RouterTransaction.with(new DialogController("Conductor", description))
.pushChangeHandler(pushHandler)
.popChangeHandler(popHandler));
}
void onModelRowClick(DemoModel model, int position) {
switch (model) {
case NAVIGATION:
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.SHOW_FOR_CHILDREN_ONLY))
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.tag(NavigationDemoController.TAG_UP_TRANSACTION)
);
break;
case TRANSITIONS:
getRouter().pushController(TransitionDemoController.getRouterTransaction(0, this));
break;
case TARGET_CONTROLLER:
getRouter().pushController(
RouterTransaction.with(new TargetDisplayController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case VIEW_PAGER:
getRouter().pushController(RouterTransaction.with(new ViewPagerController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case VIEW_PAGER_2:
getRouter().pushController(RouterTransaction.with(new ViewPager2Controller())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case CHILD_CONTROLLERS:
getRouter().pushController(RouterTransaction.with(new ParentController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case SHARED_ELEMENT_TRANSITIONS:
String titleSharedElementName = getResources().getString(R.string.transition_tag_title_indexed, position);
String dotSharedElementName = getResources().getString(R.string.transition_tag_dot_indexed, position);
getRouter().pushController(RouterTransaction.with(new CityGridController(model.title, model.color, position))
.pushChangeHandler(new ArcFadeMoveChangeHandler(titleSharedElementName, dotSharedElementName))
.popChangeHandler(new ArcFadeMoveChangeHandler(titleSharedElementName, dotSharedElementName)));
break;
case DRAG_DISMISS:
getRouter().pushController(RouterTransaction.with(new DragDismissController())
.pushChangeHandler(new FadeChangeHandler(false))
.popChangeHandler(new FadeChangeHandler()));
break;
case EXTERNAL_MODULES:
getRouter().pushController(RouterTransaction.with(new ExternalModulesController())
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
break;
case MULTIPLE_CHILD_ROUTERS:
getRouter().pushController(RouterTransaction.with(new MultipleChildRouterController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case MASTER_DETAIL:
getRouter().pushController(RouterTransaction.with(new MasterDetailListController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
}
}
class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final DemoModel[] items;
public HomeAdapter(LayoutInflater inflater, DemoModel[] items) {
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.row_home, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(position, items[position]);
}
@Override
public int getItemCount() {
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.img_dot) ImageView imgDot;
private DemoModel model;
private int position;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(int position, DemoModel item) {
model = item;
tvTitle.setText(item.title);
imgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), item.color), Mode.SRC_ATOP);
this.position = position;
ViewCompat.setTransitionName(tvTitle, getResources().getString(R.string.transition_tag_title_indexed, position));
ViewCompat.setTransitionName(imgDot, getResources().getString(R.string.transition_tag_dot_indexed, position));
}
@OnClick(R.id.row_root)
void onRowClick() {
onModelRowClick(model, position);
}
}
}
}
@@ -0,0 +1,266 @@
package com.bluelinelabs.conductor.demo.controllers
import android.content.Intent
import android.graphics.PorterDuff
import android.net.Uri
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.text.style.AbsoluteSizeSpan
import android.text.style.URLSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.asTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.changehandler.ArcFadeMoveChangeHandler
import com.bluelinelabs.conductor.demo.changehandler.FabToDialogTransitionChangeHandler
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerHomeBinding
import com.bluelinelabs.conductor.demo.databinding.RowHomeBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class HomeController : BaseController(R.layout.controller_home) {
private val binding: ControllerHomeBinding by viewBinding(ControllerHomeBinding::bind)
override val title = "Conductor Demos"
override fun onViewCreated(view: View) {
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
binding.recyclerView.adapter = HomeAdapter(
LayoutInflater.from(view.context),
DemoModel.values(),
::onModelRowClick
)
binding.fab.setOnClickListener { showAboutDialog(fromFab = true) }
}
override fun configureToolbar(toolbar: Toolbar) {
super.configureToolbar(toolbar)
toolbar.setOnMenuItemClickListener {
if (it.itemId == R.id.about) {
showAboutDialog(fromFab = false)
true
} else {
false
}
}
}
override fun configureMenu(toolbar: Toolbar) {
super.configureMenu(toolbar)
toolbar.inflateMenu(R.menu.home)
}
override fun onSaveViewState(view: View, outState: Bundle) {
super.onSaveViewState(view, outState)
outState.putInt(KEY_FAB_VISIBILITY, binding.fab.visibility)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
binding.fab.visibility = savedViewState.getInt(KEY_FAB_VISIBILITY)
}
private fun onModelRowClick(model: DemoModel, position: Int) {
when (model) {
DemoModel.NAVIGATION -> {
router.pushController(
RouterTransaction.with(NavigationDemoController(0, DisplayUpMode.SHOW_FOR_CHILDREN_ONLY))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
.tag(NavigationDemoController.TAG_UP_TRANSACTION)
)
}
DemoModel.TRANSITIONS -> {
router.pushController(TransitionDemoController.getRouterTransaction(0, this))
}
DemoModel.TARGET_CONTROLLER -> {
router.pushController(
RouterTransaction.with(TargetDisplayController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.VIEW_PAGER -> {
router.pushController(
RouterTransaction.with(ViewPagerController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.VIEW_PAGER_2 -> {
router.pushController(
RouterTransaction.with(ViewPager2Controller())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.CHILD_CONTROLLERS -> {
router.pushController(
RouterTransaction.with(ParentController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.SHARED_ELEMENT_TRANSITIONS -> {
val titleSharedElementName =
resources!!.getString(R.string.transition_tag_title_indexed, position)
val dotSharedElementName =
resources!!.getString(R.string.transition_tag_dot_indexed, position)
router.pushController(
RouterTransaction.with(CityGridController(model.title, model.color, position))
.pushChangeHandler(
ArcFadeMoveChangeHandler(
titleSharedElementName,
dotSharedElementName
)
)
.popChangeHandler(
ArcFadeMoveChangeHandler(
titleSharedElementName,
dotSharedElementName
)
)
)
}
DemoModel.DRAG_DISMISS -> {
router.pushController(
RouterTransaction.with(DragDismissController())
.pushChangeHandler(FadeChangeHandler(false))
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.EXTERNAL_MODULES -> {
router.pushController(
RouterTransaction.with(ExternalModulesController())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
DemoModel.MULTIPLE_CHILD_ROUTERS -> {
router.pushController(
RouterTransaction.with(MultipleChildRouterController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.MASTER_DETAIL -> {
router.pushController(
RouterTransaction.with(MasterDetailListController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.COMPOSE -> {
router.pushController(
ComposeController().asTransaction(FadeChangeHandler(), FadeChangeHandler())
)
}
}
}
private fun showAboutDialog(fromFab: Boolean) {
val details =
SpannableString("A small, yet full-featured framework that allows building View-based Android applications").apply {
setSpan(AbsoluteSizeSpan(16, true), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
val link = SpannableString(CONDUCTOR_URL).apply {
setSpan(object : URLSpan(CONDUCTOR_URL) {
override fun onClick(widget: View) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}, 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
val description = buildSpannedString {
append(details)
append("\n\n")
append(link)
}
val pushHandler =
if (fromFab) FabToDialogTransitionChangeHandler() else FadeChangeHandler(false)
val popHandler = if (fromFab) FabToDialogTransitionChangeHandler() else FadeChangeHandler()
router.pushController(
RouterTransaction.with(DialogController("Conductor", description))
.pushChangeHandler(pushHandler)
.popChangeHandler(popHandler)
)
}
companion object {
private const val KEY_FAB_VISIBILITY = "HomeController.fabVisibility"
private const val CONDUCTOR_URL = "https://github.com/bluelinelabs/Conductor"
}
}
private enum class DemoModel(val title: String, @ColorRes val color: Int) {
COMPOSE("Jetpack Compose", R.color.amber_500),
NAVIGATION("Navigation Demos", R.color.red_300),
TRANSITIONS("Transition Demos", R.color.blue_grey_300),
SHARED_ELEMENT_TRANSITIONS("Shared Element Demos", R.color.purple_300),
CHILD_CONTROLLERS("Child Controllers", R.color.orange_300),
VIEW_PAGER("ViewPager", R.color.green_300),
VIEW_PAGER_2("ViewPager2", R.color.pink_300),
TARGET_CONTROLLER("Target Controller", R.color.deep_orange_300),
MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.grey_300),
MASTER_DETAIL("Master Detail", R.color.lime_300),
DRAG_DISMISS("Drag Dismiss", R.color.teal_300),
EXTERNAL_MODULES("Bonus Modules", R.color.deep_purple_300)
}
private class HomeAdapter(
private val inflater: LayoutInflater,
private val items: Array<DemoModel>,
private val modelClickListener: (DemoModel, Int) -> Unit
) : RecyclerView.Adapter<HomeAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
RowHomeBinding.inflate(inflater, parent, false),
modelClickListener
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position, items[position])
}
override fun getItemCount() = items.size
class ViewHolder(
private val binding: RowHomeBinding,
private val modelClickListener: (DemoModel, Int) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(position: Int, item: DemoModel) {
binding.title.text = item.title
binding.dot.drawable.setColorFilter(
ContextCompat.getColor(binding.root.context, item.color),
PorterDuff.Mode.SRC_ATOP
)
binding.root.setOnClickListener { modelClickListener(item, position) }
binding.title.transitionName =
binding.root.resources.getString(R.string.transition_tag_title_indexed, position)
binding.dot.transitionName =
binding.root.resources.getString(R.string.transition_tag_dot_indexed, position)
}
}
}
@@ -1,158 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class MasterDetailListController extends BaseController {
private static final String KEY_SELECTED_INDEX = "MasterDetailListController.selectedIndex";
public enum DetailItemModel {
ONE("Item 1", "This is a quick demo of master/detail flow using Conductor. In portrait mode you'll see a standard list. In landscape, you'll see a two-pane layout.", R.color.green_300),
TWO("Item 2", "This is another item.", R.color.cyan_300),
THREE("Item 3", "Wow, a 3rd item!", R.color.deep_purple_300);
String title;
String detail;
int backgroundColor;
DetailItemModel(String title, String detail, int backgroundColor) {
this.title = title;
this.detail = detail;
this.backgroundColor = backgroundColor;
}
}
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@Nullable @BindView(R.id.detail_container) ViewGroup detailContainer;
private int selectedIndex;
private boolean twoPaneView;
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_master_detail_list, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new DetailItemAdapter(LayoutInflater.from(view.getContext()), DetailItemModel.values()));
twoPaneView = (detailContainer != null);
if (twoPaneView) {
onRowSelected(selectedIndex);
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_SELECTED_INDEX, selectedIndex);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
selectedIndex = savedInstanceState.getInt(KEY_SELECTED_INDEX);
}
@Override
protected String getTitle() {
return "Master/Detail Flow";
}
void onRowSelected(int index) {
selectedIndex = index;
DetailItemModel model = DetailItemModel.values()[index];
ChildController controller = new ChildController(model.detail, model.backgroundColor, true);
if (twoPaneView) {
getChildRouter(detailContainer).setRoot(RouterTransaction.with(controller));
} else {
getRouter().pushController(RouterTransaction.with(controller)
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
class DetailItemAdapter extends RecyclerView.Adapter<DetailItemAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final DetailItemModel[] items;
public DetailItemAdapter(LayoutInflater inflater, DetailItemModel[] items) {
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.row_detail_item, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items[position], position);
}
@Override
public int getItemCount() {
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.row_root) View root;
@BindView(R.id.tv_title) TextView tvTitle;
private int position;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(DetailItemModel item, int position) {
tvTitle.setText(item.title);
this.position = position;
if (twoPaneView && position == selectedIndex) {
root.setBackgroundColor(ContextCompat.getColor(root.getContext(), R.color.grey_400));
} else {
root.setBackgroundColor(ContextCompat.getColor(root.getContext(), android.R.color.transparent));
}
}
@OnClick(R.id.row_root)
void onRowClick() {
onRowSelected(position);
notifyDataSetChanged();
}
}
}
}
@@ -0,0 +1,128 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerMasterDetailListBinding
import com.bluelinelabs.conductor.demo.databinding.RowDetailItemBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class MasterDetailListController : BaseController(R.layout.controller_master_detail_list) {
private val binding: ControllerMasterDetailListBinding by viewBinding(ControllerMasterDetailListBinding::bind)
override val title = "Master/Detail Flow"
private lateinit var adapter: DetailItemAdapter
private var selectedIndex = 0
private var twoPaneView = false
private val dataModels = mutableListOf(
DetailItemModel(
"Item 1",
"This is a quick demo of master/detail flow using Conductor. In portrait mode you'll see a standard list. In landscape, you'll see a two-pane layout.",
R.color.green_300
),
DetailItemModel(
"Item 2",
"This is another item.",
R.color.cyan_300
),
DetailItemModel(
"Item 3",
"Wow, a 3rd item!",
R.color.deep_purple_300
)
)
override fun onViewCreated(view: View) {
adapter = DetailItemAdapter(LayoutInflater.from(view.context), dataModels, ::onRowSelected)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
binding.recyclerView.adapter = adapter
twoPaneView = binding.detailContainer != null
if (twoPaneView) {
onRowSelected(selectedIndex)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(KEY_SELECTED_INDEX, selectedIndex)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
selectedIndex = savedInstanceState.getInt(KEY_SELECTED_INDEX)
}
private fun onRowSelected(index: Int) {
dataModels[selectedIndex] = dataModels[selectedIndex].copy(selected = false)
selectedIndex = index
dataModels[selectedIndex] = dataModels[selectedIndex].copy(selected = true)
val model = dataModels[selectedIndex]
val controller = ChildController(model.detail, model.backgroundColor, true)
if (twoPaneView) {
getChildRouter(binding.detailContainer!!).setRoot(with(controller))
} else {
router.pushController(
with(controller)
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
}
companion object {
private const val KEY_SELECTED_INDEX = "MasterDetailListController.selectedIndex"
}
}
data class DetailItemModel(val title: String, val detail: String, val backgroundColor: Int, val selected: Boolean = false)
class DetailItemAdapter(
private val inflater: LayoutInflater,
private val items: List<DetailItemModel>,
private val rowClickListener: (Int) -> Unit
) : RecyclerView.Adapter<DetailItemAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(RowDetailItemBinding.inflate(inflater, parent, false)) { position ->
rowClickListener(position)
notifyDataSetChanged()
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount() = items.size
class ViewHolder(
private val binding: RowDetailItemBinding,
private val rowClickListener: (Int) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: DetailItemModel, position: Int) {
binding.title.text = item.title
if (item.selected) {
itemView.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.grey_400))
} else {
itemView.setBackgroundColor(ContextCompat.getColor(itemView.context, android.R.color.transparent))
}
itemView.setOnClickListener { rowClickListener(position) }
}
}
}
@@ -1,42 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.BindViews;
public class MultipleChildRouterController extends BaseController {
@BindViews({R.id.container_0, R.id.container_1, R.id.container_2}) ViewGroup[] childContainers;
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_multiple_child_routers, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
for (ViewGroup childContainer : childContainers) {
Router childRouter = getChildRouter(childContainer).setPopsLastView(false);
if (!childRouter.hasRootController()) {
childRouter.setRoot(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.HIDE)));
}
}
}
@Override
protected String getTitle() {
return "Child Router Demo";
}
}
@@ -0,0 +1,28 @@
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
import com.bluelinelabs.conductor.demo.databinding.ControllerMultipleChildRoutersBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class MultipleChildRouterController : BaseController(R.layout.controller_multiple_child_routers) {
private val binding: ControllerMultipleChildRoutersBinding by viewBinding(ControllerMultipleChildRoutersBinding::bind)
override val title = "Child Router Demo"
override fun onViewCreated(view: View) {
super.onViewCreated(view)
val childContainers = listOf(binding.container0, binding.container1, binding.container2)
childContainers.forEach { container ->
val childRouter = getChildRouter(container).setPopRootControllerMode(PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW)
if (!childRouter.hasRootController()) {
childRouter.setRoot(RouterTransaction.with(NavigationDemoController(0, NavigationDemoController.DisplayUpMode.HIDE)))
}
}
}
}
@@ -1,121 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import com.bluelinelabs.conductor.demo.util.ColorUtil;
import butterknife.BindView;
import butterknife.OnClick;
public class NavigationDemoController extends BaseController {
public enum DisplayUpMode {
SHOW,
SHOW_FOR_CHILDREN_ONLY,
HIDE;
private DisplayUpMode getDisplayUpModeForChild() {
switch (this) {
case HIDE:
return HIDE;
default:
return SHOW;
}
}
}
public static final String TAG_UP_TRANSACTION = "NavigationDemoController.up";
private static final String KEY_INDEX = "NavigationDemoController.index";
private static final String KEY_DISPLAY_UP_MODE = "NavigationDemoController.displayUpMode";
@BindView(R.id.tv_title) TextView tvTitle;
private int index;
private DisplayUpMode displayUpMode;
public NavigationDemoController(int index, DisplayUpMode displayUpMode) {
this(new BundleBuilder(new Bundle())
.putInt(KEY_INDEX, index)
.putInt(KEY_DISPLAY_UP_MODE, displayUpMode.ordinal())
.build());
}
public NavigationDemoController(Bundle args) {
super(args);
index = args.getInt(KEY_INDEX);
displayUpMode = DisplayUpMode.values()[args.getInt(KEY_DISPLAY_UP_MODE)];
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_navigation_demo, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
if (displayUpMode != DisplayUpMode.SHOW) {
view.findViewById(R.id.btn_up).setVisibility(View.GONE);
}
view.setBackgroundColor(ColorUtil.getMaterialColor(getResources(), index));
tvTitle.setText(getResources().getString(R.string.navigation_title, index));
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
setButtonsEnabled(true);
}
@Override
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeStarted(changeHandler, changeType);
setButtonsEnabled(false);
}
@Override
protected String getTitle() {
return "Navigation Demos";
}
private void setButtonsEnabled(boolean enabled) {
final View view = getView();
if (view != null) {
view.findViewById(R.id.btn_next).setEnabled(enabled);
view.findViewById(R.id.btn_up).setEnabled(enabled);
view.findViewById(R.id.btn_pop_to_root).setEnabled(enabled);
}
}
@OnClick(R.id.btn_next) void onNextClicked() {
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(index + 1, displayUpMode.getDisplayUpModeForChild()))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_up) void onUpClicked() {
getRouter().popToTag(TAG_UP_TRANSACTION);
}
@OnClick(R.id.btn_pop_to_root) void onPopToRootClicked() {
getRouter().popToRoot();
}
}
@@ -0,0 +1,88 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerNavigationDemoBinding
import com.bluelinelabs.conductor.demo.util.getMaterialColor
import com.bluelinelabs.conductor.demo.util.viewBinding
class NavigationDemoController(args: Bundle) : BaseController(R.layout.controller_navigation_demo, args) {
private val binding: ControllerNavigationDemoBinding by viewBinding(ControllerNavigationDemoBinding::bind)
private val index = args.getInt(KEY_INDEX)
private val displayUpMode = DisplayUpMode.values()[args.getInt(KEY_DISPLAY_UP_MODE)]
override val title = "Navigation Demos"
constructor(index: Int, displayUpMode: DisplayUpMode) : this(
bundleOf(
KEY_INDEX to index,
KEY_DISPLAY_UP_MODE to displayUpMode.ordinal
)
)
override fun onViewCreated(view: View) {
binding.root.setBackgroundColor(view.resources.getMaterialColor(index))
binding.goUp.isVisible = displayUpMode == DisplayUpMode.SHOW
binding.title.text = view.resources.getString(R.string.navigation_title, index)
binding.popToRoot.setOnClickListener { router.popToRoot() }
binding.goUp.setOnClickListener { router.popToTag(TAG_UP_TRANSACTION) }
binding.goToNext.setOnClickListener {
router.pushController(
RouterTransaction.with(NavigationDemoController(index + 1, displayUpMode.displayUpModeForChild))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
if (changeType.isEnter) {
setButtonsEnabled(true)
}
}
override fun onChangeStarted(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeStarted(changeHandler, changeType)
setButtonsEnabled(false)
}
private fun setButtonsEnabled(enabled: Boolean) {
binding.goToNext.isEnabled = enabled
binding.goUp.isEnabled = enabled
binding.popToRoot.isEnabled = enabled
}
companion object {
const val TAG_UP_TRANSACTION = "NavigationDemoController.up"
private const val KEY_INDEX = "NavigationDemoController.index"
private const val KEY_DISPLAY_UP_MODE = "NavigationDemoController.displayUpMode"
}
enum class DisplayUpMode {
SHOW, SHOW_FOR_CHILDREN_ONLY, HIDE;
val displayUpModeForChild: DisplayUpMode
get() = when (this) {
HIDE -> HIDE
else -> SHOW
}
}
}
@@ -1,106 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
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.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.ColorUtil;
import java.util.List;
public class ParentController extends BaseController {
private static final int NUMBER_OF_CHILDREN = 5;
private boolean finishing;
private boolean hasShownAll;
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_parent, container, false);
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
if (changeType == ControllerChangeType.PUSH_ENTER) {
addChild(0);
}
}
private void addChild(final int index) {
@IdRes final int frameId = getResources().getIdentifier("child_content_" + (index + 1), "id", getActivity().getPackageName());
final ViewGroup container = (ViewGroup)getView().findViewById(frameId);
final Router childRouter = getChildRouter(container).setPopsLastView(true);
if (!childRouter.hasRootController()) {
ChildController childController = new ChildController("Child Controller #" + index, ColorUtil.getMaterialColor(getResources(), index), false);
childController.addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!isBeingDestroyed()) {
if (changeType == ControllerChangeType.PUSH_ENTER && !hasShownAll) {
if (index < NUMBER_OF_CHILDREN - 1) {
addChild(index + 1);
} else {
hasShownAll = true;
}
} else if (changeType == ControllerChangeType.POP_EXIT) {
if (index > 0) {
removeChild(index - 1);
} else {
getRouter().popController(ParentController.this);
}
}
}
}
});
childRouter.setRoot(RouterTransaction.with(childController)
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
}
}
private void removeChild(int index) {
List<Router> childRouters = getChildRouters();
if (index < childRouters.size()) {
removeChildRouter(childRouters.get(index));
}
}
@Override
public boolean handleBack() {
int childControllers = 0;
for (Router childRouter : getChildRouters()) {
if (childRouter.hasRootController()) {
childControllers++;
}
}
if (childControllers != NUMBER_OF_CHILDREN || finishing) {
return true;
} else {
finishing = true;
return super.handleBack();
}
}
@Override
protected String getTitle() {
return "Parent/Child Demo";
}
}

Some files were not shown because too many files have changed in this diff Show More