Compare commits

...

82 Commits

Author SHA1 Message Date
Jameson Williams 56bff9a5cf Refine highlighting and error reporting in Lint rules (#693) 2024-04-10 17:00:41 -04:00
Steven Schoen 5a1746b2d3 Fix doc mistake in ControllerChangeHandler (#691) 2024-01-11 13:28:13 -05:00
Steven Schoen 2ffafaee79 Add kdoc to ControllerChangeHandler::removesFromViewOnPush (#688) 2023-09-29 23:19:21 -04:00
EricKuck eabfc005d0 Preview version bump 2023-08-15 14:50:42 -04:00
EricKuck a90ca5180e Fix issue with child controllers frozen at detach time not re-attaching 2023-08-15 14:30:19 -04:00
EricKuck 4d4d8bfbd1 Fix ConcurrentModificationException when manipulating backstack during instantiation 2023-05-09 11:09:58 -04:00
EricKuck f69fab6062 Merge LifecycleOwner into Controller, add back handling demo 2023-03-24 11:50:37 -04:00
Eric Kuck 590debf975 Conductor 4.0-preview-1 (#686) 2023-03-22 11:26:52 -05:00
EricKuck c3f7d128f5 Version bump 2023-01-30 11:56:22 -05:00
EricKuck 5e1f072672 Update maven publishing 2023-01-27 10:38:57 -05:00
EricKuck b0d15d9f9e Update maven publishing 2023-01-27 10:26:15 -05:00
Eric Kuck 8ac2e04c62 Make change start/end callbacks when parent controller is popped. Fixes #683 (#684) 2023-01-26 10:11:33 -06:00
Eric Kuck cdbdee5c42 Don't pop the final controller in pager adapters. Fixes #681 (#682) 2023-01-26 09:57:39 -06:00
EricKuck 8488242a26 Version bump 2022-11-29 15:04:22 -05:00
Eric Kuck 1fe0187439 Update ancestor change listeners to prevent memory leak (#680) 2022-11-29 13:51:25 -06:00
EricKuck f78726b916 Version bump 2022-11-07 10:38:21 -05:00
EricKuck 1f918f10c5 Fix ControllerLifecycleOwner crash when onContextAvailable was never called 2022-11-03 15:29:15 -04:00
EricKuck bd584727be Fix edge case ConcurrentModificationException 2022-09-19 16:10:39 -04:00
EricKuck 91db7fe65f Capture view reference in inflate call 2022-07-28 11:29:02 -05:00
EricKuck 2abe2b33f9 Version bump 2022-07-28 09:41:02 -05:00
Mario Noll ac4e09cf67 Fix NPE when removing view reference (#678)
Great catch, thanks!
2022-07-28 08:59:29 -05:00
EricKuck 055532bb21 Fix github actions badge 2022-07-25 17:17:08 -05:00
EricKuck 15037c2217 Version bump 2022-07-25 16:48:26 -05:00
EricKuck 728f1fb4e9 Ensure onContextUnavailable called on child routers for edge case 2022-07-22 11:55:15 -05:00
EricKuck 55c8d64d8a Fix CI badge 2022-07-05 14:46:14 -05:00
EricKuck 88e0eb882b Fix crash when a parent is transitioned out before a child can create its view 2022-07-05 10:43:52 -05:00
EricKuck 63a92db540 Pass along View's context on destroy if available 2022-07-05 10:43:06 -05:00
py - Pierre Yves Ricau ba98e3b165 Add leak detection for destroyed controller views (#676) 2022-06-23 12:00:39 -05:00
Eric Kuck 966bc1645d Convert dependencies to version catalogs (#675)
Also bumps gradle, agp, and kotlin versions
2022-06-16 16:40:05 -05:00
EricKuck c8ac58ad6a Version bump 2022-06-14 13:44:57 -05:00
Eric Kuck 5f04d9de89 Fix edge case NPE when the user hits the back button very rapidly during state restoration (#674) 2022-06-14 10:12:44 -05:00
Eric Kuck d32fc813d0 Set awaitingParentAttach to false on detach, even if not currently attached (#673) 2022-06-13 18:09:55 -05:00
Eric Kuck c2bc72c5ce Fix issue where child controllers may not get their onPause event soon enough (#672) 2022-06-13 18:09:42 -05:00
EricKuck 924e4bebfa Version bump 2022-02-18 14:28:27 -06:00
EricKuck 4ea4aa5c56 Fix issue with detach callbacks happening while not yet attached 2022-02-18 14:15:05 -06:00
Eric Kuck 3b275d31c2 Add PopRootControllerMode to address Android 12 back button behavior (#663) 2022-01-27 16:41:20 -06:00
EricKuck 0e21c8c9c1 Version bump 2021-11-30 11:39:30 -06:00
EricKuck 8297e0273d RouterStateAdapter now properly restores routers after process death 2021-11-28 11:27:47 -06:00
EricKuck 46519c2c2c Ensure preContextUnavailable can't use a null context in the parameter
Fixes #377 and #516
2021-11-23 15:50:52 -06:00
EricKuck 211da8b2ea Version bump for 3.1.1 release 2021-07-19 17:42:43 -05:00
EricKuck 26db962168 Revert maven-publish plugin migration 2021-07-19 17:41:17 -05:00
EricKuck f4c1c6ccf5 Provide a bit more safety around pushing controllers outside of normal lifecycle events 2021-07-19 17:20:32 -05:00
Mario c89caa87e0 Apply the kotlin plugin to lint module (#653)
Fixes missing IssueRegistry from lint.jar
2021-07-19 13:08:34 -05:00
Paul Woitaschek 2748566437 Make the StringSparseArrayParceler internal. 2021-07-03 22:09:39 +02:00
Paul Woitaschek 506c99ed41 Convert the StringSparseArrayParceler to kotlin. 2021-07-03 22:08:18 +02:00
Paul Woitaschek 4fe0ec5f51 Rename .java to .kt 2021-07-03 22:08:18 +02:00
Paul Woitaschek afa93f2cc1 Unit-test the StringSparseArrayParceler. 2021-07-03 22:02:08 +02:00
Paul Woitaschek 836f92b615 Update the publish task name. 2021-07-03 21:41:21 +02:00
Paul Woitaschek 94c817bbd9 Remove support for rxLifecycle2. 2021-07-03 21:40:43 +02:00
Paul Woitaschek fc1fee3e17 Kotlin backstack (#652)
Migrate the Backstack to kotlin
2021-07-03 21:32:11 +02:00
Paul Woitaschek 76b7572a01 Migrate to the maven-publish plugin. (#651) 2021-07-03 21:31:24 +02:00
Paul Woitaschek 3fc63b7f5f Remove the deprecated kotlin-android-extensions plugin and use the kotlin-parcelize plugin replacement instead. 2021-07-03 14:37:40 +02:00
Paul Woitaschek 0ef52211a2 Update LeakCanary to 2.7. (#650)
Also simplify the usage by extracting the leak watching logic into a lifecycle listener.
2021-07-03 14:31:04 +02:00
Paul Woitaschek 7574131940 Remove the unused rxJavaVersion declaration. 2021-07-02 22:40:01 +02:00
Paul Woitaschek 1ab9a4c4f6 Remove the unused rxLifecycleVersion declaration. 2021-07-02 22:35:52 +02:00
Paul Woitaschek 3bc23bd5cd Update to agp 7.0.0-beta05 2021-07-02 22:31:53 +02:00
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
EricKuck cd11ac9d6b Version bump for 3.0.0 release 2020-11-05 14:40:15 -06:00
EricKuck e78347709b Revert to Java 7 - we'll be kotlinizing soon anyway 2020-11-05 14:12:34 -06:00
EricKuck 341debc5b9 Bump back kotlin version 2020-11-04 16:59:35 -06:00
EricKuck 2346e48154 Fix errant onAttach calls for children whose parents were popped on attach
Fixes #481
Fixes #426
Fixes #407
2020-11-04 16:58:04 -06:00
EricKuck 4174e12958 Fix bug where old views could remain in the hierarchy after being popped
Fixes #608
2020-11-04 16:06:55 -06:00
EricKuck c0abed0813 Fix popping a controller during view creation
Fixes #348
2020-11-04 12:58:50 -06:00
Eric Kuck 6fdb1d6ed3 Adds ViewPager2 Adapter (#594) 2020-11-03 16:17:30 -06:00
Eric Kuck 3334b8e21f Don't remove host references on configuration changes (#629)
Should fix #567
2020-11-03 16:13:17 -06:00
Paul Woitaschek 240424dc63 Lint changehandler subclass fix (#627)
* Updated gradle to 6.7

* Updated AGP to 4.1.0 and infer lint from the agp version.

* Suppress the UnstableApiUsage on the lint tests.

* Added a test case where private change handler extends a base class that extends from the ControllerChangeHandler.

* Fixed the test case by checking the all super classes.

* Added a controller stub for the controleller detector tests.

* Added a testcase for when a controller has a base class.

* Added a testcase for when a controller has a base class.

* Accept licenses before installing for travis.

* Attempt to not add the android components.

* Removed the licenses command.

* Added the preview license too.
2020-10-27 08:23:55 +01:00
Eric Kuck f768e9ab00 Pushing to child router while unattached fixed. Closes #606 (#607) 2020-05-13 14:24:41 -05:00
Islom Matnazarov 4c89124683 Update CircularRevealChangeHandler to not ignore removesFromViewOnPush (#600) 2020-05-11 11:17:09 -05:00
Eric Kuck e0bbd48935 Remove platform transition module 2020-05-04 20:14:33 -05:00
206 changed files with 8188 additions and 9860 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
+25
View File
@@ -0,0 +1,25 @@
root = true
[*.{java,kt,kts,xml,gradle}]
charset=utf-8
end_of_line=lf
max_line_length=120
insert_final_newline=true
trim_trailing_whitespace=true
indent_style=space
[*.{java,kt,kts,gradle}]
spaces_around_operators=true
indent_brace_style=K&R
[*.{java,gradle}]
indent_size=4
[*.{kt,kts}]
indent_size=2
continuation_indent_size=2
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
[*.xml]
indent_size=2
+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: Release with Gradle
run: ./gradlew clean publishAllPublicationsToMavenCentral
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
+3 -4
View File
@@ -8,7 +8,8 @@
*.iml
*.ipr
*.iws
*.idea/dictionaries
/.idea/*
!/.idea/scopes/
classes
gen-external-apklibs
@@ -35,6 +36,4 @@ pom.xml.*
local.properties
*.prefs
# The keystore file
app/spothero-release.keystore
.DS_Store
+1
View File
@@ -6,3 +6,4 @@
/caches
/gradle.xml
/modules.xml
/compiler.xml
-129
View File
@@ -1,129 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
-6
View File
@@ -1,6 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
</state>
</component>
-35
View File
@@ -1,35 +0,0 @@
language: android
android:
components:
- tools
- build-tools-28.0.3
- android-28
- extra-android-m2repository
licenses:
- '.+'
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=
+41 -43
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
@@ -19,28 +19,30 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
Conductor 4.0 is coming soon. It is already being used in production with many, many millions of users. It is, however, not guaranteed to be API stable. As such, it is being released as a preview rather than a standard release. Preview in this context is _not_ a commentary on stability. It is considered to be up to the same quality standards as the current 3.x stable release.
Changes in Conductor 4 are available in the [GitHub releases](https://github.com/bluelinelabs/Conductor/releases/). In preparation for the release of the next version, there are currently 3 installation options:
### Latest Stable 3.x
```gradle
implementation 'com.bluelinelabs:conductor:3.0.0-rc5'
def conductorVersion = '3.2.0'
implementation "com.bluelinelabs:conductor:$conductorVersion"
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.0.0-rc5'
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.0.0-rc5'
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
// RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.0.0-rc5'
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.0.0-rc5'
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.0.0-rc5'
// ViewPager2 Adapter:
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
```
**SNAPSHOT**
### 4.0 Preview
Use `4.0.0-preview-4` as your version number in any of the dependencies above.
Just use `3.0.1-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
### SNAPSHOT
Use `4.0.0-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
```gradle
allprojects {
@@ -63,47 +65,43 @@ __RouterTransaction__ | Transactions are used to define data about adding Contro
### Minimal Activity implementation
```java
public class MainActivity extends Activity {
```kotlin
class MainActivity : AppCompatActivity() {
private Router router;
private lateinit var router: Router
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main);
setContentView(R.layout.activity_main)
ViewGroup container = (ViewGroup) findViewById(R.id.controller_container);
val container = findViewById<ViewGroup>(R.id.controller_container)
router = Conductor.attachRouter(this, container, savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new HomeController()));
}
router = Conductor.attachRouter(this, binding.controllerContainer, savedInstanceState)
.setPopRootControllerMode(PopRootControllerMode.NEVER)
.setOnBackPressedDispatcherEnabled(true)
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(HomeController()))
}
}
@Override
public void onBackPressed() {
if (!router.handleBack()) {
super.onBackPressed();
}
}
}
```
### Minimal Controller implementation
```java
public class HomeController extends Controller {
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
View view = inflater.inflate(R.layout.controller_home, container, false);
((TextView) view.findViewById(R.id.tv_title)).setText("Hello World");
return view;
}
```kotlin
class HomeController : Controller() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
val view = inflater.inflate(R.layout.controller_home, container, false)
view.findViewById<TextView>(R.id.tv_title).text = "Hello World"
return view
}
}
```
+17 -29
View File
@@ -1,41 +1,29 @@
buildscript {
apply from: rootProject.file('dependencies.gradle')
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
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.dokka
}
}
allprojects {
repositories {
mavenCentral()
maven { url 'https://maven.google.com' }
jcenter()
}
plugins.withType(com.android.build.gradle.BasePlugin).configureEach { plugin ->
plugin.extension.compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
tasks.withType(JavaCompile).configureEach { task ->
task.sourceCompatibility = JavaVersion.VERSION_1_8
task.targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile).configureEach { task ->
task.kotlinOptions {
jvmTarget = "1.8"
if (project.hasProperty('maven_publish_url')) {
pluginManager.withPlugin("com.vanniktech.maven.publish") {
publishing {
repositories {
maven {
url = maven_publish_url
credentials {
username = maven_publish_username
password = maven_publish_password
}
}
}
}
}
}
}
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')
@@ -14,12 +14,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("UnstableApiUsage")
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.UastScanner {
static final Issue ISSUE =
@@ -33,7 +31,7 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
return Collections.singletonList(UClass.class);
}
@Override
@@ -48,48 +46,41 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
final boolean hasSuperType = evaluator.extendsClass(node.getJavaPsi(), CLASS_NAME, true);
if (!hasSuperType) {
return;
}
if (!evaluator.isPublic(node)) {
String message = String.format("This ControllerChangeHandler class should be public (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
if (node.getContainingClass() != null && !evaluator.isStatic(node)) {
String message = String.format("This ControllerChangeHandler inner class should be static (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
boolean hasConstructor = false;
UMethod constructor = null;
boolean hasDefaultConstructor = false;
for (UMethod method : node.getMethods()) {
if (method.isConstructor()) {
hasConstructor = true;
if (evaluator.isPublic(method) && method.getUastParameters().size() == 0) {
constructor = method;
if (evaluator.isPublic(method) && method.getUastParameters().isEmpty()) {
hasDefaultConstructor = true;
break;
}
}
}
if (hasConstructor && !hasDefaultConstructor) {
if (constructor != null && !hasDefaultConstructor) {
String message = String.format(
"This ControllerChangeHandler needs to have a public default constructor (`%1$s`)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(constructor)), message);
}
}
};
}
}
@@ -10,18 +10,17 @@ import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UParameter;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("UnstableApiUsage")
public final class ControllerIssueDetector extends Detector implements Detector.UastScanner {
static final Issue ISSUE =
@@ -35,7 +34,7 @@ public final class ControllerIssueDetector extends Detector implements Detector.
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
return Collections.singletonList(UClass.class);
}
@Override
@@ -49,58 +48,53 @@ public final class ControllerIssueDetector extends Detector implements Detector.
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
final boolean hasSuperType = evaluator.extendsClass(node.getJavaPsi(), CLASS_NAME, true);
if (!hasSuperType) {
return;
}
if (!evaluator.isPublic(node)) {
String message = String.format("This Controller class should be public (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
if (node.getContainingClass() != null && !evaluator.isStatic(node)) {
String message = String.format("This Controller inner class should be static (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
boolean hasConstructor = false;
UMethod constructor = null;
boolean hasDefaultConstructor = false;
boolean hasBundleConstructor = false;
for (UMethod method : node.getMethods()) {
if (method.isConstructor()) {
hasConstructor = true;
constructor = method;
if (evaluator.isPublic(method)) {
List<UParameter> parameters = method.getUastParameters();
if (parameters.size() == 0) {
if (parameters.isEmpty()) {
hasDefaultConstructor = true;
break;
} else if (parameters.size() == 1 &&
(parameters.get(0).getType().equalsToText(SdkConstants.CLASS_BUNDLE)) ||
parameters.get(0).getType().equalsToText("Bundle")) {
hasBundleConstructor = true;
} else if (parameters.size() == 1) {
PsiType type = parameters.get(0).getType();
if (type.equalsToText(SdkConstants.CLASS_BUNDLE) || type.equalsToText("Bundle")) {
hasBundleConstructor = true;
break;
}
}
}
}
}
if (hasConstructor && !hasDefaultConstructor && !hasBundleConstructor) {
if (constructor != null && !hasDefaultConstructor && !hasBundleConstructor) {
String message = String.format(
"This Controller needs to have either a public default constructor or a" +
" public single-argument constructor that takes a Bundle. (`%1$s`)",
node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(constructor)), message);
}
}
};
}
}
@@ -0,0 +1,15 @@
package com.bluelinelabs.conductor.lint;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNameIdentifierOwner;
final class Identify {
private Identify() {}
static PsiElement byName(PsiNameIdentifierOwner psiNameIdentifierOwner) {
if (psiNameIdentifierOwner.getNameIdentifier() != null) {
return psiNameIdentifierOwner.getNameIdentifier();
}
return psiNameIdentifierOwner;
}
}
@@ -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,23 +1,19 @@
package com.bluelinelabs.conductor.lint;
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;
public class ControllerChangeHandlerDetectorTest {
private static final String CONSTRUCTOR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private static final String PRIVATE_CLASS_ERROR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private final TestFile controllerChangeHandlerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class ControllerChangeHandler {}"
);
@Test
public void testWithNoConstructor() {
@@ -27,7 +23,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -42,7 +38,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -57,10 +53,15 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR);
.expect(""
+ "src/test/SampleHandler.java:3: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ " public SampleHandler(int number) { }\n"
+ " ~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -73,7 +74,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -88,10 +89,15 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR);
.expect(""
+ "src/test/SampleHandler.java:3: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ " private SampleHandler() { }\n"
+ " ~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -103,10 +109,32 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(PRIVATE_CLASS_ERROR);
.expect("src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ " ~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n");
}
@Test
public void testWithPrivateClassOfBaseClass() {
@Language("JAVA") String baseClass = ""
+ "package test;\n"
+ "abstract class BaseChangeHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {}";
@Language("JAVA") String source = ""
+ "package test;\n"
+ "private class SampleHandler extends test.BaseChangeHandler {}";
lint()
.files(controllerChangeHandlerStub, java(baseClass), java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect("src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n" +
"private class SampleHandler extends test.BaseChangeHandler {}\n" +
" ~~~~~~~~~~~~~\n" +
"1 errors, 0 warnings");
}
}
@@ -1,24 +1,27 @@
package com.bluelinelabs.conductor.lint;
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;
public class ControllerDetectorTest {
private static final String CONSTRUCTOR_ERROR =
"src/test/SampleController.java:2: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private static final String CLASS_ERROR =
"src/test/SampleController.java:2: Error: This Controller class should be public (test.SampleController) [ValidController]\n"
+ "private class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "^\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n";
private final TestFile controllerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class Controller {}"
);
@Test
public void testWithNoConstructor() {
@Language("JAVA") String source = ""
@@ -27,7 +30,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -42,7 +45,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -57,10 +60,15 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR_ERROR);
.expect(""
+ "src/test/SampleController.java:3: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ " public SampleController(int number) { }\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -73,12 +81,38 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
}
@Test
public void testWithBaseClassAndPrivateConstructor() {
@Language("JAVA")
String baseClass = ""
+ "package test;\n"
+ "public class BaseController extends com.bluelinelabs.conductor.Controller {}";
@Language("JAVA")
String source = ""
+ "package test;\n"
+ "public class SampleController extends BaseController {\n"
+ " private SampleController() { }\n"
+ "}";
lint()
.files(controllerStub, java(baseClass), java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(""
+ "src/test/SampleController.java:3: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ " private SampleController() { }\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings"
);
}
@Test
public void testWithPrivateConstructor() {
@Language("JAVA") String source = ""
@@ -88,10 +122,15 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR_ERROR);
.expect(""
+ "src/test/SampleController.java:3: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ " private SampleController() { }\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -103,7 +142,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CLASS_ERROR);
@@ -0,0 +1,24 @@
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
implementation libs.androidx.appcompat
implementation libs.androidx.collection
api libs.androidx.transition
implementation project(':conductor')
}
ext.artifactId = 'conductor-androidx-transition'
@@ -1,3 +1,3 @@
POM_NAME=Conductor AndroidX Transition Extensions
POM_ARTIFACT_ID=conductor-transition-androidx
POM_ARTIFACT_ID=conductor-androidx-transition
POM_PACKAGING=aar
@@ -113,7 +113,7 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
}
@Override
public boolean removesFromViewOnPush() {
public boolean getRemovesFromViewOnPush() {
return true;
}
@@ -142,7 +142,7 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) {
if (from != null && (getRemovesFromViewOnPush() || !isPush) && from.getParent() == container) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
@@ -1,23 +0,0 @@
apply plugin: 'com.android.library'
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.archComponentsLifecycle
implementation project(':conductor')
}
ext.artifactId = 'conductor-arch-components-lifecycle'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -1,3 +0,0 @@
POM_NAME=Conductor Architecture Components Lifecycle Extensions
POM_ARTIFACT_ID=conductor-archlifecycle
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.archlifecycle">
<application />
</manifest>
@@ -1,65 +0,0 @@
package com.bluelinelabs.conductor.archlifecycle;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
public class ControllerLifecycleOwner implements LifecycleOwner {
private final LifecycleRegistry lifecycleRegistry;
public <T extends Controller & LifecycleOwner> ControllerLifecycleOwner(@NonNull T lifecycleController) {
lifecycleRegistry = new LifecycleRegistry(lifecycleController); // --> State.INITIALIZED
lifecycleController.addLifecycleListener(new LifecycleListener() {
@Override
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_CREATE); // --> State.CREATED;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_START); // --> State.STARTED;
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME); // --> State.RESUMED;
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_PAUSE); // --> State.STARTED;
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_STOP); // --> State.CREATED;
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
// do nothing
}
@Override
public void preDestroy(@NonNull Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
}
});
}
@Override @NonNull
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
}
@@ -1,28 +0,0 @@
package com.bluelinelabs.conductor.archlifecycle;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
public abstract class LifecycleController extends Controller implements LifecycleOwner {
private final ControllerLifecycleOwner lifecycleOwner = new ControllerLifecycleOwner(this);
public LifecycleController() {
super();
}
public LifecycleController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
public Lifecycle getLifecycle() {
return lifecycleOwner.getLifecycle();
}
}
@@ -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.autodispose
api rootProject.ext.autodisposeLifecycle
implementation project(':conductor')
}
ext.artifactId = 'conductor-autodispose'
@@ -1,3 +0,0 @@
POM_NAME=Conductor AutoDispose Extensions
POM_ARTIFACT_ID=conductor-autodispose
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.autodispose">
<application />
</manifest>
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,71 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.OutsideScopeException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
@NonNull
public static BehaviorSubject<ControllerEvent> create(@NonNull Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideScopeException("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,81 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.OutsideScopeException;
import com.uber.autodispose.lifecycle.LifecycleScopeProvider;
import com.uber.autodispose.lifecycle.LifecycleScopes;
import com.uber.autodispose.lifecycle.CorrespondingEventsFunction;
import io.reactivex.CompletableSource;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerScopeProvider implements LifecycleScopeProvider<ControllerEvent> {
private static final CorrespondingEventsFunction<ControllerEvent> CORRESPONDING_EVENTS =
new CorrespondingEventsFunction<ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) throws OutsideScopeException {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case ATTACH:
return ControllerEvent.DETACH;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideScopeException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
@NonNull private final BehaviorSubject<ControllerEvent> lifecycleSubject;
@NonNull private final CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction;
public static ControllerScopeProvider from(@NonNull Controller controller) {
return new ControllerScopeProvider(controller, CORRESPONDING_EVENTS);
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final ControllerEvent untilEvent) {
return new ControllerScopeProvider(controller, new CorrespondingEventsFunction<ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent controllerEvent) {
return untilEvent;
}
});
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction) {
return new ControllerScopeProvider(controller, correspondingEventsFunction);
}
private ControllerScopeProvider(@NonNull Controller controller, @NonNull CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction) {
lifecycleSubject = ControllerLifecycleSubjectHelper.create(controller);
this.correspondingEventsFunction = correspondingEventsFunction;
}
@Override
public Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
public CorrespondingEventsFunction<ControllerEvent> correspondingEvents() {
return correspondingEventsFunction;
}
@Override
public ControllerEvent peekLifecycle() {
return lifecycleSubject.getValue();
}
@Override
public CompletableSource requestScope() throws Exception {
return LifecycleScopes.resolveScopeFromLifecycle(this);
}
}
@@ -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.");
}
}
};
}
@@ -1,24 +0,0 @@
apply plugin: 'com.android.library'
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 {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxCollection
api rootProject.ext.androidxTransition
implementation project(':conductor')
}
ext.artifactId = 'conductor-androidx-transition'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -1,21 +0,0 @@
apply plugin: 'com.android.library'
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 {
implementation project(':conductor')
}
ext.artifactId = 'conductor-android-transition'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -1,3 +0,0 @@
POM_NAME=Conductor Platform Transition Extensions
POM_ARTIFACT_ID=conductor-transition-platform
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.platformtransition">
<application />
</manifest>
@@ -1,647 +0,0 @@
package com.bluelinelabs.conductor.changehandler.platformtransition;
import android.annotation.TargetApi;
import android.app.SharedElementCallback;
import android.graphics.Rect;
import android.os.Build;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionSet;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.TransitionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* A TransitionChangeHandler that facilitates using different Transitions for the entering view, the exiting view,
* and shared elements between the two.
* <p/>
* Note that this class uses Android's <b>platform</b> {@link Transition}. If you're using androidx transitions, consider
* using the {@code SharedElementTransitionChangeHandler} provided by the {@code androidx-transitions} Conductor module.
*/
// Much of this class is based on FragmentTransition.java and FragmentTransitionCompat21.java from the Android support library
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class SharedElementTransitionChangeHandler extends TransitionChangeHandler {
// A map of from -> to names. Generally these will be the same.
@NonNull final ArrayMap<String, String> sharedElementNames = new ArrayMap<>();
@NonNull final List<String> waitForTransitionNames = new ArrayList<>();
@NonNull final List<ViewParentPair> removedViews = new ArrayList<>();
@Nullable Transition exitTransition;
@Nullable Transition enterTransition;
@Nullable Transition sharedElementTransition;
@Nullable private SharedElementCallback exitTransitionCallback;
@Nullable private SharedElementCallback enterTransitionCallback;
@NonNull
@Override
protected final Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
exitTransition = getExitTransition(container, from, to, isPush);
enterTransition = getEnterTransition(container, from, to, isPush);
sharedElementTransition = getSharedElementTransition(container, from, to, isPush);
exitTransitionCallback = getExitTransitionCallback(container, from, to, isPush);
enterTransitionCallback = getEnterTransitionCallback(container, from, to, isPush);
if (enterTransition == null && sharedElementTransition == null && exitTransition == null) {
throw new IllegalStateException("SharedElementTransitionChangeHandler must have at least one transaction.");
}
return mergeTransitions(isPush);
}
@Override
public void prepareForTransition(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, @NonNull final Transition transition, final boolean isPush, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
OnTransitionPreparedListener listener = () -> {
configureTransition(container, from, to, transition, isPush);
onTransitionPreparedListener.onPrepared();
};
configureSharedElements(container, from, to, isPush);
if (to != null && to.getParent() == null && waitForTransitionNames.size() > 0) {
waitOnAllTransitionNames(to, listener);
container.addView(to);
} else {
listener.onPrepared();
}
}
@Override
public final void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (to != null && removedViews.size() > 0) {
to.setVisibility(View.VISIBLE);
for (ViewParentPair removedView : removedViews) {
removedView.parent.addView(removedView.view);
}
removedViews.clear();
}
super.executePropertyChanges(container, from, to, transition, isPush);
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
removedViews.clear();
}
@Override
protected void onEnd() {
exitTransition = null;
enterTransition = null;
sharedElementTransition = null;
}
void configureTransition(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, @NonNull final Transition transition, boolean isPush) {
final View nonExistentView = new View(container.getContext());
List<View> fromSharedElements = new ArrayList<>();
List<View> toSharedElements = new ArrayList<>();
configureSharedElements(container, nonExistentView, to, from, isPush, fromSharedElements, toSharedElements);
List<View> exitingViews = exitTransition != null ? configureEnteringExitingViews(exitTransition, from, fromSharedElements, nonExistentView) : null;
if (exitingViews == null || exitingViews.isEmpty()) {
exitTransition = null;
}
if (enterTransition != null) {
enterTransition.addTarget(nonExistentView);
}
final List<View> enteringViews = new ArrayList<>();
scheduleRemoveTargets(transition, enterTransition, enteringViews, exitTransition, exitingViews, sharedElementTransition, toSharedElements);
scheduleTargetChange(container, to, nonExistentView, toSharedElements, enteringViews, exitingViews);
setNameOverrides(container, toSharedElements);
scheduleNameReset(container, toSharedElements);
}
private void waitOnAllTransitionNames(@NonNull final View to, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
OnPreDrawListener onPreDrawListener = new OnPreDrawListener() {
boolean addedSubviewListeners;
@Override
public boolean onPreDraw() {
List<View> foundViews = new ArrayList<>();
boolean allViewsFound = true;
for (String transitionName : waitForTransitionNames) {
View namedView = TransitionUtils.findNamedView(to, transitionName);
if (namedView != null) {
foundViews.add(TransitionUtils.findNamedView(to, transitionName));
} else {
allViewsFound = false;
break;
}
}
if (allViewsFound && !addedSubviewListeners) {
addedSubviewListeners = true;
waitOnChildTransitionNames(to, foundViews, this, onTransitionPreparedListener);
}
return false;
}
};
to.getViewTreeObserver().addOnPreDrawListener(onPreDrawListener);
}
void waitOnChildTransitionNames(@NonNull final View to, @NonNull List<View> foundViews, @NonNull final OnPreDrawListener parentPreDrawListener, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
for (final View view : foundViews) {
OneShotPreDrawListener.add(true, view, () -> {
waitForTransitionNames.remove(view.getTransitionName());
removedViews.add(new ViewParentPair(view, (ViewGroup) view.getParent()));
((ViewGroup) view.getParent()).removeView(view);
if (waitForTransitionNames.size() == 0) {
to.getViewTreeObserver().removeOnPreDrawListener(parentPreDrawListener);
to.setVisibility(View.INVISIBLE);
onTransitionPreparedListener.onPrepared();
}
});
}
}
private void scheduleTargetChange(@NonNull final ViewGroup container, @Nullable final View to, @NonNull final View nonExistentView,
@NonNull final List<View> toSharedElements, @NonNull final List<View> enteringViews, @Nullable final List<View> exitingViews) {
OneShotPreDrawListener.add(true, container, () -> {
if (enterTransition != null) {
enterTransition.removeTarget(nonExistentView);
List<View> views = configureEnteringExitingViews(enterTransition, to, toSharedElements, nonExistentView);
enteringViews.addAll(views);
}
if (exitingViews != null) {
if (exitTransition != null) {
List<View> tempExiting = new ArrayList<>();
tempExiting.add(nonExistentView);
TransitionUtils.replaceTargets(exitTransition, exitingViews, tempExiting);
}
exitingViews.clear();
exitingViews.add(nonExistentView);
}
});
}
private Transition mergeTransitions(boolean isPush) {
boolean overlap = enterTransition == null || exitTransition == null || allowTransitionOverlap(isPush);
if (overlap) {
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, exitTransition, enterTransition, sharedElementTransition);
} else {
Transition staggered = TransitionUtils.mergeTransitions(TransitionSet.ORDERING_SEQUENTIAL, exitTransition, enterTransition);
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, staggered, sharedElementTransition);
}
}
@NonNull List<View> configureEnteringExitingViews(@NonNull Transition transition, @Nullable View view, @NonNull List<View> sharedElements, @NonNull View nonExistentView) {
List<View> viewList = new ArrayList<>();
if (view != null) {
captureTransitioningViews(viewList, view);
}
viewList.removeAll(sharedElements);
if (!viewList.isEmpty()) {
viewList.add(nonExistentView);
TransitionUtils.addTargets(transition, viewList);
}
return viewList;
}
private void configureSharedElements(@NonNull ViewGroup container, @NonNull final View nonExistentView, @Nullable final View to, @Nullable View from,
final boolean isPush, @NonNull final List<View> fromSharedElements, @NonNull final List<View> toSharedElements) {
if (to == null || from == null) {
return;
}
ArrayMap<String, View> capturedFromSharedElements = captureFromSharedElements(from);
if (sharedElementNames.isEmpty()) {
sharedElementTransition = null;
} else if (capturedFromSharedElements != null) {
fromSharedElements.addAll(capturedFromSharedElements.values());
}
if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
return;
}
callSharedElementStartEnd(capturedFromSharedElements, true);
final Rect toEpicenter;
if (sharedElementTransition != null) {
toEpicenter = new Rect();
TransitionUtils.setTargets(sharedElementTransition, nonExistentView, fromSharedElements);
setFromEpicenter(capturedFromSharedElements);
if (enterTransition != null) {
enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
@Override
public Rect onGetEpicenter(Transition transition) {
if (toEpicenter.isEmpty()) {
return null;
}
return toEpicenter;
}
});
}
} else {
toEpicenter = null;
}
OneShotPreDrawListener.add(true, container, () -> {
ArrayMap<String, View> capturedToSharedElements = captureToSharedElements(to, isPush);
if (capturedToSharedElements != null) {
toSharedElements.addAll(capturedToSharedElements.values());
toSharedElements.add(nonExistentView);
}
callSharedElementStartEnd(capturedToSharedElements, false);
if (sharedElementTransition != null) {
sharedElementTransition.getTargets().clear();
sharedElementTransition.getTargets().addAll(toSharedElements);
TransitionUtils.replaceTargets(sharedElementTransition, fromSharedElements, toSharedElements);
final View toEpicenterView = getToEpicenterView(capturedToSharedElements);
if (toEpicenterView != null && toEpicenter != null) {
TransitionUtils.getBoundsOnScreen(toEpicenterView, toEpicenter);
}
}
});
}
@Nullable View getToEpicenterView(@Nullable ArrayMap<String, View> toSharedElements) {
if (enterTransition != null && sharedElementNames.size() > 0 && toSharedElements != null) {
return toSharedElements.get(sharedElementNames.valueAt(0));
}
return null;
}
private void setFromEpicenter(@Nullable ArrayMap<String, View> fromSharedElements) {
if (sharedElementNames.size() > 0 && fromSharedElements != null) {
final View fromEpicenterView = fromSharedElements.get(sharedElementNames.keyAt(0));
if (sharedElementTransition != null) {
TransitionUtils.setEpicenter(sharedElementTransition, fromEpicenterView);
}
if (exitTransition != null) {
TransitionUtils.setEpicenter(exitTransition, fromEpicenterView);
}
}
}
@Nullable ArrayMap<String, View> captureToSharedElements(@Nullable final View to, boolean isPush) {
if (sharedElementNames.isEmpty() || sharedElementTransition == null || to == null) {
sharedElementNames.clear();
return null;
}
final ArrayMap<String, View> toSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(toSharedElements, to);
for (ViewParentPair removedView : removedViews) {
toSharedElements.put(removedView.view.getTransitionName(), removedView.view);
}
final List<String> names = new ArrayList<>(sharedElementNames.values());
toSharedElements.retainAll(names);
if (enterTransitionCallback != null) {
enterTransitionCallback.onMapSharedElements(names, toSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = toSharedElements.get(name);
if (view == null) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.remove(key);
}
} else if (!name.equals(view.getTransitionName())) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.put(key, view.getTransitionName());
}
}
}
} else {
for (int i = sharedElementNames.size() - 1; i >= 0; i--) {
final String targetName = sharedElementNames.valueAt(i);
if (!toSharedElements.containsKey(targetName)) {
sharedElementNames.removeAt(i);
}
}
}
return toSharedElements;
}
@Nullable String findKeyForValue(@NonNull ArrayMap<String, String> map, @NonNull String value) {
final int numElements = map.size();
for (int i = 0; i < numElements; i++) {
if (value.equals(map.valueAt(i))) {
return map.keyAt(i);
}
}
return null;
}
@Nullable
private ArrayMap<String, View> captureFromSharedElements(@NonNull View from) {
if (sharedElementNames.isEmpty() || sharedElementTransition == null) {
sharedElementNames.clear();
return null;
}
final ArrayMap<String, View> fromSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(fromSharedElements, from);
final List<String> names = new ArrayList<>(sharedElementNames.keySet());
fromSharedElements.retainAll(names);
if (exitTransitionCallback != null) {
exitTransitionCallback.onMapSharedElements(names, fromSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = fromSharedElements.get(name);
if (view == null) {
sharedElementNames.remove(name);
} else if (!name.equals(view.getTransitionName())) {
String targetValue = sharedElementNames.remove(name);
sharedElementNames.put(view.getTransitionName(), targetValue);
}
}
} else {
sharedElementNames.retainAll(fromSharedElements.keySet());
}
return fromSharedElements;
}
void callSharedElementStartEnd(@Nullable ArrayMap<String, View> sharedElements, boolean isStart) {
if (enterTransitionCallback != null) {
final int count = sharedElements == null ? 0 : sharedElements.size();
List<View> views = new ArrayList<>(count);
List<String> names = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
names.add(sharedElements.keyAt(i));
views.add(sharedElements.valueAt(i));
}
if (isStart) {
enterTransitionCallback.onSharedElementStart(names, views, null);
} else {
enterTransitionCallback.onSharedElementEnd(names, views, null);
}
}
}
private void captureTransitioningViews(@NonNull List<View> transitioningViews, @NonNull View view) {
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
if (viewGroup.isTransitionGroup()) {
transitioningViews.add(viewGroup);
} else {
int count = viewGroup.getChildCount();
for (int i = 0; i < count; i++) {
View child = viewGroup.getChildAt(i);
captureTransitioningViews(transitioningViews, child);
}
}
} else {
transitioningViews.add(view);
}
}
}
private void scheduleRemoveTargets(@NonNull final Transition overallTransition,
@Nullable final Transition enterTransition, @Nullable final List<View> enteringViews,
@Nullable final Transition exitTransition, @Nullable final List<View> exitingViews,
@Nullable final Transition sharedElementTransition, @Nullable final List<View> toSharedElements) {
overallTransition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
if (enterTransition != null && enteringViews != null) {
TransitionUtils.replaceTargets(enterTransition, enteringViews, null);
}
if (exitTransition != null && exitingViews != null) {
TransitionUtils.replaceTargets(exitTransition, exitingViews, null);
}
if (sharedElementTransition != null && toSharedElements != null) {
TransitionUtils.replaceTargets(sharedElementTransition, toSharedElements, null);
}
}
@Override
public void onTransitionEnd(Transition transition) { }
@Override
public void onTransitionCancel(Transition transition) { }
@Override
public void onTransitionPause(Transition transition) { }
@Override
public void onTransitionResume(Transition transition) { }
});
}
private void setNameOverrides(@NonNull final View container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, () -> {
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
View view = toSharedElements.get(i);
String name = view.getTransitionName();
if (name != null) {
String inName = findKeyForValue(sharedElementNames, name);
view.setTransitionName(inName);
}
}
});
}
private void scheduleNameReset(@NonNull final ViewGroup container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, () -> {
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
final View view = toSharedElements.get(i);
final String name = view.getTransitionName();
final String inName = sharedElementNames.get(name);
view.setTransitionName(inName);
}
});
}
/**
* Will be called when views are ready to have their shared elements configured. Within this method one of the addSharedElement methods
* should be called for each shared element that will be used. If one or more of these shared elements will not instantly be available in
* the incoming view (for ex, in a RecyclerView), waitOnSharedElementNamed can be called to delay the transition until everything is available.
*/
public abstract void configureSharedElements(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the exiting ("from") view, if one is desired.
*/
@Nullable
public abstract Transition getExitTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on shared elements between the from and to views.
*/
@Nullable
public abstract Transition getSharedElementTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the entering ("to") view, if one is desired.
*/
@Nullable
public abstract Transition getEnterTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "from" view.
*/
@Nullable
public SharedElementCallback getExitTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "to" view.
*/
@Nullable
public SharedElementCallback getEnterTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return whether or not the the exit transition and enter transition should overlap. If true,
* the enter transition will start as soon as possible. Otherwise, the enter transition will wait until the
* completion of the exit transition. Defaults to true.
*/
public boolean allowTransitionOverlap(boolean isPush) {
return true;
}
/**
* Used to register an element that will take part in the shared element transition.
*
* @param name The transition name that is used for both the entering and exiting views.
*/
protected final void addSharedElement(@NonNull String name) {
sharedElementNames.put(name, name);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param fromName The transition name used in the "from" view
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull String fromName, @NonNull String toName) {
sharedElementNames.put(fromName, toName);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param sharedElement The view from the "from" view that will take part in the shared element transition
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull View sharedElement, @NonNull String toName) {
String transitionName = sharedElement.getTransitionName();
if (transitionName == null) {
throw new IllegalArgumentException("Unique transitionNames are required for all sharedElements");
}
sharedElementNames.put(transitionName, toName);
}
/**
* The transition will be delayed until the view with the name passed in is available in the "to" hierarchy. This is
* particularly useful for views that don't load instantly, like RecyclerViews. Note that using this method can
* potentially lock up your app indefinitely if the view never loads!
*/
protected final void waitOnSharedElementNamed(@NonNull String name) {
if (!sharedElementNames.values().contains(name)) {
throw new IllegalStateException("Can't wait on a shared element that hasn't been registered using addSharedElement");
}
waitForTransitionNames.add(name);
}
private static class OneShotPreDrawListener implements OnPreDrawListener, View.OnAttachStateChangeListener {
private final View view;
private ViewTreeObserver viewTreeObserver;
private final Runnable runnable;
private final boolean preDrawReturnValue;
private OneShotPreDrawListener(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
this.preDrawReturnValue = preDrawReturnValue;
this.view = view;
viewTreeObserver = view.getViewTreeObserver();
this.runnable = runnable;
}
@NonNull
public static OneShotPreDrawListener add(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
OneShotPreDrawListener listener = new OneShotPreDrawListener(preDrawReturnValue, view, runnable);
view.getViewTreeObserver().addOnPreDrawListener(listener);
view.addOnAttachStateChangeListener(listener);
return listener;
}
@Override
public boolean onPreDraw() {
removeListener();
runnable.run();
return preDrawReturnValue;
}
private void removeListener() {
if (viewTreeObserver.isAlive()) {
viewTreeObserver.removeOnPreDrawListener(this);
} else {
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
view.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewAttachedToWindow(View v) {
viewTreeObserver = v.getViewTreeObserver();
}
@Override
public void onViewDetachedFromWindow(View v) {
removeListener();
}
}
private static class ViewParentPair {
@NonNull final View view;
@NonNull final ViewGroup parent;
ViewParentPair(@NonNull View view, ViewGroup parent) {
this.view = view;
this.parent = parent;
}
}
}
@@ -1,149 +0,0 @@
package com.bluelinelabs.conductor.changehandler.platformtransition;
import android.annotation.TargetApi;
import android.os.Build;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A base {@link ControllerChangeHandler} that facilitates using {@link Transition}s to replace Controller Views.
* <p/>
* Note that this class uses Android's <b>platform</b> {@link Transition}. If you're using androidx transitions, consider
* using the {@code TransitionChangeHandler} provided by the {@code androidx-transitions} Conductor module.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public interface OnTransitionPreparedListener {
void onPrepared();
}
boolean canceled;
private boolean needsImmediateCompletion;
/**
* Should be overridden to return the Transition to use while replacing Views.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop
*/
@NonNull
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
}
@Override
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
}
@Nullable
private ControllerChangeCompletedListener listener;
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
listener = changeListener;
if (canceled) {
changeListener.onChangeCompleted();
return;
}
if (needsImmediateCompletion) {
executePropertyChanges(container, from, to, null, isPush);
changeListener.onChangeCompleted();
return;
}
final Runnable onTransitionNotStarted = changeListener::onChangeCompleted;
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
container.removeCallbacks(onTransitionNotStarted);
}
@Override
public void onTransitionEnd(Transition transition) {
listener.onChangeCompleted();
listener = null;
}
@Override
public void onTransitionCancel(Transition transition) {
listener.onChangeCompleted();
listener = null;
}
@Override
public void onTransitionPause(Transition transition) { }
@Override
public void onTransitionResume(Transition transition) { }
});
prepareForTransition(container, from, to, transition, isPush, () -> {
if (!canceled) {
TransitionManager.beginDelayedTransition(container, transition);
executePropertyChanges(container, from, to, transition, isPush);
container.post(onTransitionNotStarted);
}
});
}
@Override
public boolean removesFromViewOnPush() {
return true;
}
/**
* Called before a transition occurs. This can be used to reorder views, set their transition names, etc. The transition will begin
* when {@code onTransitionPreparedListener} is called.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition that is being prepared for
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) {
onTransitionPreparedListener.onPrepared();
}
/**
* This should set all view properties needed for the transition to work properly. By default it removes the "from" view
* and adds the "to" view.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called. This will be null only if another ControllerChangeHandler immediately overrides this one.
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
}
@@ -1,184 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import java.util.Map;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class TransitionUtils {
public static void findNamedViews(@NonNull Map<String, View> namedViews, View view) {
if (view.getVisibility() == View.VISIBLE) {
String transitionName = view.getTransitionName();
if (transitionName != null) {
namedViews.put(transitionName, view);
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = viewGroup.getChildAt(i);
findNamedViews(namedViews, child);
}
}
}
}
@Nullable
public static View findNamedView(@NonNull View view, @NonNull String transitionName) {
if (transitionName.equals(view.getTransitionName())) {
return view;
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View viewWithTransitionName = findNamedView(viewGroup.getChildAt(i), transitionName);
if (viewWithTransitionName != null) {
return viewWithTransitionName;
}
}
}
return null;
}
public static void setEpicenter(@NonNull Transition transition, @Nullable View view) {
if (view != null) {
final Rect epicenter = new Rect();
getBoundsOnScreen(view, epicenter);
transition.setEpicenterCallback(new Transition.EpicenterCallback() {
@Override
public Rect onGetEpicenter(Transition transition) {
return epicenter;
}
});
}
}
public static void getBoundsOnScreen(@NonNull View view, @NonNull Rect epicenter) {
int[] loc = new int[2];
view.getLocationOnScreen(loc);
epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
}
public static void setTargets(@NonNull Transition transition, @NonNull View nonExistentView, @NonNull List<View> sharedViews) {
final List<View> views = transition.getTargets();
views.clear();
final int count = sharedViews.size();
for (int i = 0; i < count; i++) {
final View view = sharedViews.get(i);
bfsAddViewChildren(views, view);
}
views.add(nonExistentView);
sharedViews.add(nonExistentView);
addTargets(transition, sharedViews);
}
public static void addTargets(@Nullable Transition transition, @NonNull List<View> views) {
if (transition == null) {
return;
}
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
int numTransitions = set.getTransitionCount();
for (int i = 0; i < numTransitions; i++) {
Transition child = set.getTransitionAt(i);
addTargets(child, views);
}
} else if (!hasSimpleTarget(transition)) {
List<View> targets = transition.getTargets();
if (isNullOrEmpty(targets)) {
int numViews = views.size();
for (int i = 0; i < numViews; i++) {
transition.addTarget(views.get(i));
}
}
}
}
public static void replaceTargets(@NonNull Transition transition, @NonNull List<View> oldTargets, @Nullable List<View> newTargets) {
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
int numTransitions = set.getTransitionCount();
for (int i = 0; i < numTransitions; i++) {
Transition child = set.getTransitionAt(i);
replaceTargets(child, oldTargets, newTargets);
}
} else if (!hasSimpleTarget(transition)) {
List<View> targets = transition.getTargets();
if (targets != null && targets.size() == oldTargets.size() && targets.containsAll(oldTargets)) {
final int targetCount = newTargets == null ? 0 : newTargets.size();
for (int i = 0; i < targetCount; i++) {
transition.addTarget(newTargets.get(i));
}
for (int i = oldTargets.size() - 1; i >= 0; i--) {
transition.removeTarget(oldTargets.get(i));
}
}
}
}
private static void bfsAddViewChildren(@NonNull final List<View> views, @NonNull final View startView) {
final int startIndex = views.size();
if (containedBeforeIndex(views, startView, startIndex)) {
return; // This child is already in the list, so all its children are also.
}
views.add(startView);
for (int index = startIndex; index < views.size(); index++) {
final View view = views.get(index);
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
final int childCount = viewGroup.getChildCount();
for (int childIndex = 0; childIndex < childCount; childIndex++) {
final View child = viewGroup.getChildAt(childIndex);
if (!containedBeforeIndex(views, child, startIndex)) {
views.add(child);
}
}
}
}
}
private static boolean containedBeforeIndex(@NonNull List<View> views, View view, int maxIndex) {
for (int i = 0; i < maxIndex; i++) {
if (views.get(i) == view) {
return true;
}
}
return false;
}
public static boolean hasSimpleTarget(@NonNull Transition transition) {
return !isNullOrEmpty(transition.getTargetIds())
|| !isNullOrEmpty(transition.getTargetNames())
|| !isNullOrEmpty(transition.getTargetTypes());
}
private static boolean isNullOrEmpty(@Nullable List list) {
return list == null || list.isEmpty();
}
@NonNull
public static TransitionSet mergeTransitions(int ordering, Transition... transitions) {
TransitionSet transitionSet = new TransitionSet();
for (Transition transition : transitions) {
if (transition != null) {
transitionSet.addTransition(transition);
}
}
transitionSet.setOrdering(ordering);
return transitionSet;
}
}
+11 -11
View File
@@ -1,26 +1,26 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.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"
@@ -6,6 +6,10 @@ import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
@@ -15,12 +19,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
/**
* An adapter for ViewPagers that uses Routers as pages
* An ViewPager adapter that uses Routers as pages
*/
public abstract class RouterPagerAdapter extends PagerAdapter {
@@ -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;
@@ -77,7 +77,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
savedPages.remove(position);
}
Router router = host.getChildRouter(container, name);
Router router = host.getChildRouter(container, name)
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER);
if (!router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
@@ -103,7 +104,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 +122,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 +140,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,13 +1,6 @@
package com.bluelinelabs.conductor.viewpager
import android.app.Activity
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.viewpager.util.FakePager
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
@@ -19,43 +12,25 @@ import org.robolectric.annotation.Config
@Config(manifest = Config.NONE)
class StateSaveTests {
private val pager: FakePager
private val pagerAdapter: RouterPagerAdapter
private val testController = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.testController()
init {
val activityController = Robolectric.buildActivity(Activity::class.java).setup()
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
val controller = TestController()
router.setRoot(with(controller))
pager = FakePager(FrameLayout(activityController.get()).also {
it.id = ViewCompat.generateViewId()
})
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
}
}
pager.setAdapter(pagerAdapter)
}
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.pageTo(i)
pager.currentItem = i
}
pager.pageTo(pagerAdapter.count / 2)
// Ensure all non-visible pages are saved
assertEquals(
pagerAdapter.count - 1 - (pager.offscreenPageLimit * 2),
destroyedItems.size,
pagerAdapter.savedPages.size()
)
}
@@ -67,38 +42,37 @@ class StateSaveTests {
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.pageTo(i)
pager.currentItem = i
}
val firstSelectedItem = pagerAdapter.count / 2
pager.pageTo(firstSelectedItem)
for (i in pagerAdapter.count downTo firstSelectedItem) {
pager.currentItem = i
}
var savedPages = pagerAdapter.savedPages
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size())
// Ensure correct pages are saved
assertEquals(
pagerAdapter.count - 3,
savedPages.keyAt(0)
)
assertEquals(
pagerAdapter.count - 2,
savedPages.keyAt(1)
)
assertEquals(
pagerAdapter.count - 1,
savedPages.keyAt(2)
)
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages.keyAt(0))
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages.keyAt(1))
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages.keyAt(2))
val secondSelectedItem = 1
pager.pageTo(secondSelectedItem)
for (i in firstSelectedItem downTo secondSelectedItem) {
pager.currentItem = i
}
savedPages = pagerAdapter.savedPages
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size())
// Ensure correct pages are saved
assertEquals(firstSelectedItem - 1, savedPages.keyAt(0))
assertEquals(firstSelectedItem, savedPages.keyAt(1))
assertEquals(firstSelectedItem + 1, savedPages.keyAt(2))
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages.keyAt(0))
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages.keyAt(1))
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages.keyAt(2))
}
}
@@ -1,61 +0,0 @@
package com.bluelinelabs.conductor.viewpager.util;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class FakePager {
private ViewGroup container;
private int offscreenPageLimit;
private final SparseArray<Object> pages = new SparseArray<>();
private RouterPagerAdapter adapter;
public FakePager(ViewGroup container) {
this.container = container;
}
public void setAdapter(RouterPagerAdapter adapter) {
this.adapter = adapter;
}
public void pageTo(int page) {
int firstPage = Math.max(0, page - offscreenPageLimit);
int lastPage = Math.min(adapter.getCount() - 1, page + offscreenPageLimit);
List<Integer> pagesI = new ArrayList<>();
for (int i = 0; i < pages.size(); i++) {
pagesI.add(pages.keyAt(i));
}
for (int i = pages.size() - 1; i >= 0; i--) {
int key = pages.keyAt(i);
if (key < firstPage || key > lastPage) {
adapter.destroyItem(container, key, pages.get(key));
pages.remove(key);
}
}
for (int key = firstPage; key <= lastPage; key++) {
if (pages.get(key) == null) {
pages.put(key, adapter.instantiateItem(container, key));
}
}
adapter.setPrimaryItem(container, page, pages.get(page));
}
public int getOffscreenPageLimit() {
return offscreenPageLimit;
}
public void setOffscreenPageLimit(int offscreenPageLimit) {
this.offscreenPageLimit = offscreenPageLimit;
}
}
@@ -0,0 +1,85 @@
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.core.view.ViewCompat
import androidx.fragment.app.FragmentActivity
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 : FragmentActivity() {
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)
}
}
+34
View File
@@ -0,0 +1,34 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation libs.junit
testImplementation libs.robolectric
implementation libs.androidx.appcompat
implementation libs.androidx.viewpager2
implementation project(':conductor')
}
ext.artifactId = 'conductor-viewpager2'
@@ -0,0 +1,3 @@
POM_NAME=Conductor ViewPager2 Adapter
POM_ARTIFACT_ID=conductor-viewpager2
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.viewpager2">
<application />
</manifest>
@@ -0,0 +1,256 @@
package com.bluelinelabs.conductor.viewpager2
import android.os.Bundle
import android.os.Parcelable
import android.util.LongSparseArray
import android.util.SparseArray
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.StatefulAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import kotlinx.parcelize.Parcelize
/**
* An ViewPager2 adapter that uses Routers as pages
*/
abstract class RouterStateAdapter(private val host: Controller) :
RecyclerView.Adapter<RouterViewHolder>(), StatefulAdapter {
private var savedPages = LongSparseArray<Bundle>()
internal var savedPageHistory = mutableListOf<Long>()
private var maxPagesToStateSave = Int.MAX_VALUE
private val visibleRouters = SparseArray<Router>()
private var currentPrimaryRouterPosition = 0
private var primaryItemCallback: PrimaryItemCallback? = null
init {
super.setHasStableIds(true)
}
/**
* Called when a router is instantiated. Here the router's root should be set if needed.
*
* @param router The router used for the page
* @param position The page position to be instantiated.
*/
abstract fun configureRouter(router: Router, position: Int)
/**
* Sets the maximum number of pages that will have their states saved. When this number is exceeded,
* the page that was state saved least recently will have its state removed from the save data.
*/
open fun setMaxPagesToStateSave(maxPagesToStateSave: Int) {
require(maxPagesToStateSave >= 0) { "Only positive integers may be passed for maxPagesToStateSave." }
this.maxPagesToStateSave = maxPagesToStateSave
ensurePagesSaved()
}
private fun inferViewPager(recyclerView: RecyclerView): ViewPager2 {
return recyclerView.parent as? ViewPager2
?: error("Expected ViewPager2 instance. Got: ${recyclerView.parent}")
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
val viewPager = inferViewPager(recyclerView)
primaryItemCallback = PrimaryItemCallback().also {
viewPager.registerOnPageChangeCallback(it)
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
val viewPager = inferViewPager(recyclerView)
primaryItemCallback?.let {
viewPager.unregisterOnPageChangeCallback(it)
}
primaryItemCallback = null
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RouterViewHolder {
return RouterViewHolder(parent)
}
override fun onBindViewHolder(holder: RouterViewHolder, position: Int) {
holder.currentItemPosition = position
attachRouter(holder, position)
}
override fun onViewAttachedToWindow(holder: RouterViewHolder) {
super.onViewAttachedToWindow(holder)
if (!holder.attached) {
attachRouter(holder, holder.currentItemPosition)
}
}
override fun onViewDetachedFromWindow(holder: RouterViewHolder) {
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) {
super.onViewRecycled(holder)
detachRouter(holder)
holder.currentRouter?.let { router ->
host.removeChildRouter(router)
holder.currentRouter = null
}
}
override fun onFailedToRecycleView(holder: RouterViewHolder): Boolean {
return true
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun setHasStableIds(hasStableIds: Boolean) {
throw UnsupportedOperationException("Stable Ids are required for the adapter to function properly")
}
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) },
savedPageHistory = savedPageHistory,
maxPagesToStateSave = maxPagesToStateSave
)
}
override fun restoreState(state: Parcelable) {
if (state !is SavedState) return
savedPages = LongSparseArray()
state.savedPagesKeys.indices.forEach { index ->
savedPages.put(state.savedPagesKeys[index], state.savedPagesValues[index])
}
savedPageHistory = state.savedPageHistory.toMutableList()
maxPagesToStateSave = state.maxPagesToStateSave
}
private fun attachRouter(holder: RouterViewHolder, position: Int) {
val itemId = getItemId(position)
val router = host.getChildRouter(holder.container, "$itemId", true, false)!!
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
// This should have already been handled by onViewRecycled, but it seems like this wasn't
// always reliably called
if (router != holder.currentRouter) {
holder.currentRouter?.let { host.removeChildRouter(it) }
}
holder.currentRouter = router
holder.currentItemId = itemId
if (!router.hasRootController()) {
val routerSavedState = savedPages[itemId]
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState)
savedPages.remove(itemId)
savedPageHistory.remove(itemId)
}
}
router.rebindIfNeeded()
configureRouter(router, position)
if (position != currentPrimaryRouterPosition) {
for (transaction in router.backstack) {
transaction.controller.setOptionsMenuHidden(true)
}
}
visibleRouters.put(position, router)
holder.attached = true
}
private fun detachRouter(holder: RouterViewHolder) {
if (!holder.attached) {
return
}
holder.currentRouter?.let { router ->
router.prepareForHostDetach()
savePage(holder.currentItemId, router)
if (visibleRouters[holder.currentItemPosition] == router) {
visibleRouters.remove(holder.currentItemPosition)
}
}
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 routerIdToRemove = savedPageHistory.removeAt(0)
savedPages.remove(routerIdToRemove)
}
}
/**
* Returns the already instantiated Router in the specified position or `null` if there
* is no router associated with this position.
*/
fun getRouter(position: Int): Router? {
return visibleRouters[position]
}
inner class PrimaryItemCallback : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val router = visibleRouters[position]
if (position != currentPrimaryRouterPosition) {
val previousRouter = visibleRouters[currentPrimaryRouterPosition]
previousRouter?.backstack?.forEach { it.controller.setOptionsMenuHidden(true) }
router?.backstack?.forEach { it.controller.setOptionsMenuHidden(false) }
currentPrimaryRouterPosition = position
}
}
}
@Parcelize
private data class SavedState(
val savedPagesKeys: List<Long>,
val savedPagesValues: List<Bundle>,
val savedPageHistory: List<Long>,
val maxPagesToStateSave: Int
) : Parcelable
}
@@ -0,0 +1,27 @@
package com.bluelinelabs.conductor.viewpager2
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
import com.bluelinelabs.conductor.Router
class RouterViewHolder private constructor(val container: ChangeHandlerFrameLayout) : ViewHolder(container) {
var currentRouter: Router? = null
var currentItemPosition = 0
var currentItemId = 0L
var attached = false
companion object {
operator fun invoke(parent: ViewGroup): RouterViewHolder {
val container = ChangeHandlerFrameLayout(parent.context)
container.id = ViewCompat.generateViewId()
container.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
container.isSaveEnabled = false
return RouterViewHolder(container)
}
}
}
@@ -0,0 +1,119 @@
package com.bluelinelabs.conductor.viewpager2
import android.os.Looper.getMainLooper
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.viewpager2.util.TestController
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: ViewPager2
private val adapter: RouterStateAdapter
private val destroyedItems = mutableListOf<Int>()
init {
val activityController = Robolectric.buildActivity(FragmentActivity::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 = ViewPager2(activityController.get()).also {
it.id = ViewCompat.generateViewId()
}
layout.addView(pager)
pager.offscreenPageLimit = 1
adapter = object : RouterStateAdapter(controller) {
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
router.setRoot(with(TestController()))
}
}
override fun getItemCount(): Int {
return 20
}
override fun onViewDetachedFromWindow(holder: RouterViewHolder) {
super.onViewDetachedFromWindow(holder)
destroyedItems.add(holder.currentItemPosition)
}
}
pager.adapter = adapter
shadowOf(getMainLooper()).idle()
}
@Test
fun testNoMaxSaves() {
// Load all pages
for (i in 0 until adapter.itemCount) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
// Ensure all non-visible pages are saved
assertEquals(
destroyedItems.size,
adapter.savedPageHistory.size
)
}
@Test
fun testMaxSavedSet() {
val maxPages = 3
adapter.setMaxPagesToStateSave(maxPages)
// Load all pages
for (i in 0 until adapter.itemCount) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
val firstSelectedItem = adapter.itemCount / 2
for (i in adapter.itemCount downTo firstSelectedItem) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
var savedPages = adapter.savedPageHistory
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size)
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages[savedPages.lastIndex].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages[savedPages.lastIndex - 1].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages[savedPages.lastIndex - 2].toInt())
val secondSelectedItem = 1
for (i in adapter.itemCount downTo secondSelectedItem) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
savedPages = adapter.savedPageHistory
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size)
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages[savedPages.lastIndex].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages[savedPages.lastIndex - 1].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages[savedPages.lastIndex - 2].toInt())
}
}
@@ -1,4 +1,4 @@
package com.bluelinelabs.conductor.viewpager.util;
package com.bluelinelabs.conductor.viewpager2.util;
import android.os.Bundle;
import android.view.LayoutInflater;
+19 -12
View File
@@ -1,12 +1,16 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
consumerProguardFiles 'proguard-rules.txt'
@@ -14,16 +18,19 @@ android {
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
api libs.androidx.activity
api libs.androidx.appcompat
api libs.androidx.savedstate.ktx
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.kotest
api rootProject.ext.androidxAnnotations
api kotlinStd
api 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"
@@ -5,9 +5,10 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
@@ -20,6 +21,10 @@ public class ActivityHostedRouter extends Router {
private LifecycleHandler lifecycleHandler;
private final TransactionIndexer transactionIndexer = new TransactionIndexer();
public ActivityHostedRouter() {
popRootControllerMode = PopRootControllerMode.NEVER;
}
public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) {
if (this.lifecycleHandler != lifecycleHandler || this.container != container) {
if (this.container != null && this.container instanceof ControllerChangeListener) {
@@ -57,15 +62,18 @@ public class ActivityHostedRouter extends Router {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
lifecycleHandler = null;
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
super.onActivityDestroyed(activity, isConfigurationChange);
if (!isConfigurationChange) {
lifecycleHandler = null;
}
}
@Override
public final void invalidateOptionsMenu() {
if (lifecycleHandler != null && lifecycleHandler.getFragmentManager() != null) {
lifecycleHandler.getFragmentManager().invalidateOptionsMenu();
if (lifecycleHandler != null && getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
@@ -81,7 +89,7 @@ public class ActivityHostedRouter extends Router {
@Override
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode);
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, null);
}
@Override
@@ -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,102 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import java.util.ArrayDeque
import java.util.Deque
internal class Backstack : Iterable<RouterTransaction> {
private val backstack: Deque<RouterTransaction> = ArrayDeque()
val isEmpty: Boolean get() = backstack.isEmpty()
val size: Int get() = backstack.size
var onBackstackUpdatedListener: OnBackstackUpdatedListener? = null
fun root(): RouterTransaction? = backstack.lastOrNull()
override fun iterator(): Iterator<RouterTransaction> = backstack.toTypedArray().iterator()
fun reverseIterator(): Iterator<RouterTransaction> = backstack.reversed().iterator()
fun remove(transaction: RouterTransaction) = backstack.remove(transaction)
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 {
onBackstackUpdatedListener?.onBackstackUpdated()
it.controller.destroy()
}
}
fun peek(): RouterTransaction? = backstack.peek()
fun push(transaction: RouterTransaction) {
backstack.push(transaction)
onBackstackUpdatedListener?.onBackstackUpdated()
}
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)
}
onBackstackUpdatedListener?.onBackstackUpdated()
}
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!!))
}
}
onBackstackUpdatedListener?.onBackstackUpdated()
}
fun interface OnBackstackUpdatedListener {
fun onBackstackUpdated()
}
companion object {
private const val KEY_ENTRIES = "Backstack.entries"
}
}
@@ -7,26 +7,35 @@ import androidx.annotation.UiThread
import com.bluelinelabs.conductor.internal.LifecycleHandler
import com.bluelinelabs.conductor.internal.ensureMainThread
/**
* Conductor will create a [Router] that has been initialized for your Activity and containing ViewGroup.
* If an existing [Router] is already associated with this Activity/ViewGroup pair, either in memory
* or in the savedInstanceState, that router will be used and rebound instead of creating a new one with
* an empty backstack.
*
* @param activity The Activity that will host the [Router] being attached.
* @param container The ViewGroup in which the [Router]'s [Controller] views will be hosted
* @param savedInstanceState The savedInstanceState passed into the hosting Activity's onCreate method. Used
* for restoring the Router's state if possible.
* @return A fully configured [Router] instance for use with this Activity/ViewGroup pair.
*/
@UiThread
object Conductor {
/**
* Conductor will create a [Router] that has been initialized for your Activity and containing ViewGroup.
* If an existing [Router] is already associated with this Activity/ViewGroup pair, either in memory
* or in the savedInstanceState, that router will be used and rebound instead of creating a new one with
* an empty backstack.
*
* @param activity The Activity that will host the [Router] being attached.
* @param container The ViewGroup in which the [Router]'s [Controller] views will be hosted
* @param savedInstanceState The savedInstanceState passed into the hosting Activity's onCreate method. Used
* for restoring the Router's state if possible.
* @param allowExperimentalAndroidXBacking Use AndroidX backing if true and if the activity parameter is a
* FragmentActivity.
* @return A fully configured [Router] instance for use with this Activity/ViewGroup pair.
*/
@UiThread
@JvmStatic
fun attachRouter(activity: Activity, container: ViewGroup, savedInstanceState: Bundle?): Router {
@JvmOverloads
fun attachRouter(
activity: Activity,
container: ViewGroup,
savedInstanceState: Bundle?,
allowExperimentalAndroidXBacking: Boolean = true,
): Router {
ensureMainThread()
return LifecycleHandler.install(activity)
return LifecycleHandler.install(activity, allowAndroidXBacking = allowExperimentalAndroidXBacking)
.getRouter(container, savedInstanceState)
.also { it.rebindIfNeeded() }
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
}
}
@@ -1,6 +1,5 @@
package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -9,7 +8,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;
@@ -18,11 +16,17 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.ComponentActivity;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import com.bluelinelabs.conductor.internal.ClassUtils;
import com.bluelinelabs.conductor.internal.ControllerLifecycleOwner;
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;
@@ -79,6 +83,7 @@ public abstract class Controller {
private boolean awaitingParentAttach;
private boolean hasSavedViewState;
boolean isDetachFrozen;
boolean onBackPressedDispatcherEnabled;
private ControllerChangeHandler overriddenPushHandler;
private ControllerChangeHandler overriddenPopHandler;
private RetainViewMode retainViewMode = RetainViewMode.RELEASE_DETACH;
@@ -91,6 +96,25 @@ public abstract class Controller {
private boolean isPerformingExitTransition;
private boolean isContextAvailable;
final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
// Root-level routers should have PopRootControllerMode.NEVER, and so should never return false here.
// This is meant to handle higher-level pops only, where the predictive back gesture doesn't come into play.
if (!router.getRootRouter().handleBackDispatch()) {
// Disable to ensure we don't have an infinite call loop.
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
if (!isBeingDestroyed) {
setEnabled(true);
}
}
}
};
public final LifecycleOwner lifecycleOwner = new ControllerLifecycleOwner(this);
@NonNull
static Controller newInstance(@NonNull Bundle bundle) {
final String className = bundle.getString(KEY_CLASS_NAME);
@@ -141,6 +165,7 @@ public abstract class Controller {
this.args = args != null ? args : new Bundle(getClass().getClassLoader());
instanceId = UUID.randomUUID().toString();
ensureRequiredConstructor();
OwnViewTreeLifecycleAndRegistry.Companion.own(this);
}
/**
@@ -212,6 +237,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.");
@@ -219,7 +261,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;
}
@@ -227,8 +269,8 @@ public abstract class Controller {
if (childRouter == null) {
if (createIfNeeded) {
childRouter = new ControllerHostedRouter(container.getId(), tag);
childRouter.setHost(this, container);
childRouter = new ControllerHostedRouter(container.getId(), tag, boundToHostContainerId);
childRouter.setHostContainer(this, container);
childRouters.add(childRouter);
if (isPerformingExitTransition) {
@@ -236,7 +278,7 @@ public abstract class Controller {
}
}
} else if (!childRouter.hasHost()) {
childRouter.setHost(this, container);
childRouter.setHostContainer(this, container);
childRouter.rebindIfNeeded();
}
@@ -294,6 +336,17 @@ public abstract class Controller {
return router != null ? router.getActivity() : null;
}
/**
* Returns the OnBackPressedDispatcher for this Controller's {@link Router} or {@code null} if:
* - This Router has not yet been attached to an Activity
* - The attached Activity does not extend ComponentActivity
* - The Activity has been destroyed
*/
@Nullable
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
return router != null ? router.getOnBackPressedDispatcher() : null;
}
/**
* Returns the Resources from the host Activity or {@code null} if this Controller has not
* yet been attached to an Activity or if the Activity has been destroyed.
@@ -573,7 +626,6 @@ public abstract class Controller {
* including {@link #shouldShowRequestPermissionRationale(String)} and
* {@link #onRequestPermissionsResult(int, String[], int[])} will be forwarded back to this Controller by the system.
*/
@TargetApi(Build.VERSION_CODES.M)
public final void requestPermissions(@NonNull final String[] permissions, final int requestCode) {
requestedPermissions.addAll(Arrays.asList(permissions));
@@ -603,8 +655,11 @@ public abstract class Controller {
/**
* Should be overridden if this Controller needs to handle the back button being pressed.
*
* Note: This method has been deprecated and should be replaced with registering an OnBackPressedCallback.
*
* @return True if this Controller has consumed the back button press, otherwise false
*/
@Deprecated
public boolean handleBack() {
List<RouterTransaction> childTransactions = new ArrayList<>();
@@ -612,7 +667,7 @@ public abstract class Controller {
childTransactions.addAll(childRouter.getBackstack());
}
Collections.sort(childTransactions, (o1, o2) -> o2.getTransactionIndex() - o1.getTransactionIndex());
Collections.sort(childTransactions, (t1, t2) -> t2.getTransactionIndex() - t1.getTransactionIndex());
for (RouterTransaction transaction : childTransactions) {
Controller childController = transaction.controller();
@@ -660,7 +715,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);
}
}
@@ -809,6 +864,14 @@ public abstract class Controller {
lifecycleListener.preContextAvailable(this);
}
onBackPressedDispatcherEnabled = router.onBackPressedDispatcherEnabled;
if (onBackPressedDispatcherEnabled) {
if (!(context instanceof ComponentActivity)) {
throw new IllegalStateException("Host activities must extend ComponentActivity when enabling OnBackPressedDispatcher support.");
}
getOnBackPressedDispatcher().addCallback(onBackPressedCallback);
}
isContextAvailable = true;
onContextAvailable(context);
@@ -823,6 +886,31 @@ 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();
if (onBackPressedDispatcherEnabled) {
onBackPressedCallback.remove();
}
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
}
}
final void executeWithRouter(@NonNull RouterRequiringFunc listener) {
if (router != null) {
listener.execute();
@@ -875,20 +963,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) {
@@ -938,7 +1013,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();
@@ -948,34 +1023,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);
}
@@ -987,7 +1069,11 @@ public abstract class Controller {
onDestroyView(view);
viewAttachHandler.unregisterAttachListener(view);
// viewAttachHandler may be null iff the controller was popped before we got here
if (viewAttachHandler != null) {
viewAttachHandler.unregisterAttachListener(view);
}
viewAttachHandler = null;
viewIsAttached = false;
@@ -1007,14 +1093,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) {
@@ -1036,33 +1123,35 @@ public abstract class Controller {
restoreViewState(view);
viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() {
@Override
public void onAttached() {
viewIsAttached = true;
viewWasDetached = false;
attach(view);
}
@Override
public void onDetached(boolean fromActivityStop) {
viewIsAttached = false;
viewWasDetached = true;
if (!isDetachFrozen) {
detach(view, false, fromActivityStop);
if (!isBeingDestroyed) {
viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() {
@Override
public void onAttached() {
viewIsAttached = true;
viewWasDetached = false;
attach(view);
}
}
@Override
public void onViewDetachAfterStop() {
if (!isDetachFrozen) {
detach(view, false, false);
@Override
public void onDetached(boolean fromActivityStop) {
viewIsAttached = false;
viewWasDetached = true;
if (!isDetachFrozen) {
detach(view, false, fromActivityStop);
}
}
}
});
viewAttachHandler.listenForAttach(view);
} else if (retainViewMode == RetainViewMode.RETAIN_DETACH) {
@Override
public void onViewDetachAfterStop() {
if (!isDetachFrozen) {
detach(view, false, false);
}
}
});
viewAttachHandler.listenForAttach(view);
}
} else {
restoreChildControllerHosts();
}
@@ -1074,28 +1163,21 @@ public abstract class Controller {
if (!childRouter.hasHost()) {
View containerView = view.findViewById(childRouter.getHostId());
if (containerView != null && containerView instanceof ViewGroup) {
childRouter.setHost(this, (ViewGroup) containerView);
if (containerView instanceof ViewGroup) {
childRouter.setHostContainer(this, (ViewGroup) containerView);
childRouter.rebindIfNeeded();
}
}
}
}
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) {
@@ -1133,7 +1215,7 @@ public abstract class Controller {
}
if (!attached) {
removeViewReference();
removeViewReference(null);
} else if (removeViews) {
detach(view, true, false);
}
@@ -1234,6 +1316,7 @@ public abstract class Controller {
List<Bundle> childBundles = savedInstanceState.getParcelableArrayList(KEY_CHILD_ROUTERS);
for (Bundle childBundle : childBundles) {
ControllerHostedRouter childRouter = new ControllerHostedRouter();
childRouter.setHostController(this);
childRouter.restoreInstanceState(childBundle);
childRouters.add(childRouter);
}
@@ -1304,11 +1387,17 @@ public abstract class Controller {
if (isDetachFrozen != frozen) {
isDetachFrozen = frozen;
boolean detach = !frozen && view != null && viewWasDetached;
for (ControllerHostedRouter router : childRouters) {
if (detach) {
router.prepareForHostDetach();
}
router.setDetachFrozen(frozen);
}
if (!frozen && view != null && viewWasDetached) {
if (detach) {
View aView = view;
detach(view, false, false);
if (view == null && aView.getParent() == router.container) {
@@ -1382,66 +1471,66 @@ public abstract class Controller {
/**
* Allows external classes to listen for lifecycle events in a Controller
*/
public interface LifecycleListener {
public static abstract class LifecycleListener {
default void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
default void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
default void preCreateView(@NonNull Controller controller) {
public void preCreateView(@NonNull Controller controller) {
}
default void postCreateView(@NonNull Controller controller, @NonNull View view) {
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
}
default void preAttach(@NonNull Controller controller, @NonNull View view) {
public void preAttach(@NonNull Controller controller, @NonNull View view) {
}
default void postAttach(@NonNull Controller controller, @NonNull View view) {
public void postAttach(@NonNull Controller controller, @NonNull View view) {
}
default void preDetach(@NonNull Controller controller, @NonNull View view) {
public void preDetach(@NonNull Controller controller, @NonNull View view) {
}
default void postDetach(@NonNull Controller controller, @NonNull View view) {
public void postDetach(@NonNull Controller controller, @NonNull View view) {
}
default void preDestroyView(@NonNull Controller controller, @NonNull View view) {
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
}
default void postDestroyView(@NonNull Controller controller) {
public void postDestroyView(@NonNull Controller controller) {
}
default void preDestroy(@NonNull Controller controller) {
public void preDestroy(@NonNull Controller controller) {
}
default void postDestroy(@NonNull Controller controller) {
public void postDestroy(@NonNull Controller controller) {
}
default void preContextAvailable(@NonNull Controller controller) {
public void preContextAvailable(@NonNull Controller controller) {
}
default void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
}
default void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
}
default void postContextUnavailable(@NonNull Controller controller) {
public void postContextUnavailable(@NonNull Controller controller) {
}
default void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
}
default void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
}
default void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
}
default void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
}
}
@@ -1,321 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ControllerChangeHandlers are responsible for swapping the View for one Controller to the View
* of another. They can be useful for performing animations and transitions between Controllers. Several
* default ControllerChangeHandlers are included.
*/
public abstract class ControllerChangeHandler {
private static final String KEY_CLASS_NAME = "ControllerChangeHandler.className";
private static final String KEY_SAVED_STATE = "ControllerChangeHandler.savedState";
static final Map<String, ChangeHandlerData> inProgressChangeHandlers = new HashMap<>();
boolean forceRemoveViewOnPush;
private boolean hasBeenUsed;
/**
* Responsible for swapping Views from one Controller to another.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param changeListener This listener must be called when any transitions or animations are completed.
*/
public abstract void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public ControllerChangeHandler() {
ensureDefaultConstructor();
}
/**
* Saves any data about this handler to a Bundle in case the application is killed.
*
* @param bundle The Bundle into which data should be stored.
*/
public void saveToBundle(@NonNull Bundle bundle) {
}
/**
* Restores data that was saved in the {@link #saveToBundle(Bundle bundle)} method.
*
* @param bundle The bundle that has data to be restored
*/
public void restoreFromBundle(@NonNull Bundle bundle) {
}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* popped before it has completed.
*
* @param newHandler The change handler that has caused this push to be aborted
* @param newTop The Controller that will now be at the top of the backstack or {@code null}
* if there will be no new Controller at the top
*/
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* needs to be attached immediately, without any animations or transitions.
*/
public void completeImmediately() {
}
/**
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
* will be saved and restored from the Bundle format.
*/
@NonNull
public ControllerChangeHandler copy() {
return fromBundle(toBundle());
}
/**
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
* In the case that a handler is not reusable, it will be copied using the {@link #copy()} method
* prior to use.
*/
public boolean isReusable() {
return false;
}
@NonNull
final Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(KEY_CLASS_NAME, getClass().getName());
Bundle savedState = new Bundle();
saveToBundle(savedState);
bundle.putBundle(KEY_SAVED_STATE, savedState);
return bundle;
}
private void ensureDefaultConstructor() {
try {
getClass().getConstructor();
} catch (Exception e) {
throw new RuntimeException(getClass() + " does not have a default constructor.");
}
}
@Nullable
public static ControllerChangeHandler fromBundle(@Nullable Bundle bundle) {
if (bundle != null) {
String className = bundle.getString(KEY_CLASS_NAME);
ControllerChangeHandler changeHandler = ClassUtils.newInstance(className);
//noinspection ConstantConditions
changeHandler.restoreFromBundle(bundle.getBundle(KEY_SAVED_STATE));
return changeHandler;
} else {
return null;
}
}
static boolean completeHandlerImmediately(@NonNull String controllerInstanceId) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(controllerInstanceId);
if (changeHandlerData != null) {
changeHandlerData.changeHandler.completeImmediately();
inProgressChangeHandlers.remove(controllerInstanceId);
return true;
}
return false;
}
static void abortOrComplete(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(toAbort.getInstanceId());
if (changeHandlerData != null) {
if (changeHandlerData.isPush) {
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController);
} else {
changeHandlerData.changeHandler.completeImmediately();
}
inProgressChangeHandlers.remove(toAbort.getInstanceId());
}
}
static void executeChange(@NonNull final ChangeTransaction transaction) {
executeChange(transaction.to, transaction.from, transaction.isPush, transaction.container, transaction.changeHandler, transaction.listeners);
}
private static void executeChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ViewGroup container, @Nullable final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
if (container != null) {
final ControllerChangeHandler handler;
if (inHandler == null) {
handler = new SimpleSwapChangeHandler();
} else if (inHandler.hasBeenUsed && !inHandler.isReusable()) {
handler = inHandler.copy();
} else {
handler = inHandler;
}
handler.hasBeenUsed = true;
if (from != null) {
if (isPush) {
completeHandlerImmediately(from.getInstanceId());
} else {
abortOrComplete(from, to, handler);
}
}
if (to != null) {
inProgressChangeHandlers.put(to.getInstanceId(), new ChangeHandlerData(handler, isPush));
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeStarted(to, from, isPush, container, handler);
}
final ControllerChangeType toChangeType = isPush ? ControllerChangeType.PUSH_ENTER : ControllerChangeType.POP_ENTER;
final ControllerChangeType fromChangeType = isPush ? ControllerChangeType.PUSH_EXIT : ControllerChangeType.POP_EXIT;
final View toView;
if (to != null) {
toView = to.inflate(container);
to.changeStarted(handler, toChangeType);
} else {
toView = null;
}
final View fromView;
if (from != null) {
fromView = from.getView();
from.changeStarted(handler, fromChangeType);
} else {
fromView = null;
}
handler.performChange(container, fromView, toView, isPush, () -> {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
if (to != null) {
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, handler);
}
if (handler.forceRemoveViewOnPush && fromView != null) {
ViewParent fromParent = fromView.getParent();
if (fromParent != null && fromParent instanceof ViewGroup) {
((ViewGroup) fromParent).removeView(fromView);
}
}
if (handler.removesFromViewOnPush() && from != null) {
from.setNeedsAttach(false);
}
});
}
}
protected void onEnd() {
}
public boolean removesFromViewOnPush() {
return true;
}
public void setForceRemoveViewOnPush(boolean force) {
forceRemoveViewOnPush = force;
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
public interface ControllerChangeListener {
/**
* Called when a {@link ControllerChangeHandler} has started changing {@link Controller}s
*
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this is a push operation, or false if it's a pop.
* @param container The containing ViewGroup
* @param handler The change handler being used.
*/
default void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
}
/**
* Called when a {@link ControllerChangeHandler} has completed changing {@link Controller}s
*
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this was a push operation, or false if it's a pop
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
default void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
}
}
static class ChangeTransaction {
@Nullable
final Controller to;
@Nullable
final Controller from;
final boolean isPush;
@Nullable
final ViewGroup container;
@Nullable
final ControllerChangeHandler changeHandler;
@NonNull
final List<ControllerChangeListener> listeners;
public ChangeTransaction(@Nullable Controller to, @Nullable Controller from, boolean isPush, @Nullable ViewGroup container, @Nullable ControllerChangeHandler changeHandler, @NonNull List<ControllerChangeListener> listeners) {
this.to = to;
this.from = from;
this.isPush = isPush;
this.container = container;
this.changeHandler = changeHandler;
this.listeners = listeners;
}
}
/**
* A simplified listener for being notified when the change is complete. This MUST be called by any custom
* ControllerChangeHandlers in order to ensure that {@link Controller}s will be notified of this change.
*/
public interface ControllerChangeCompletedListener {
/**
* Called when the change is complete.
*/
void onChangeCompleted();
}
private static class ChangeHandlerData {
public final ControllerChangeHandler changeHandler;
public final boolean isPush;
public ChangeHandlerData(ControllerChangeHandler changeHandler, boolean isPush) {
this.changeHandler = changeHandler;
this.isPush = isPush;
}
}
}
@@ -0,0 +1,329 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RestrictTo
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
import com.bluelinelabs.conductor.internal.ClassUtils
/**
* ControllerChangeHandlers are responsible for swapping the View for one Controller to the View
* of another. They can be useful for performing animations and transitions between Controllers. Several
* default ControllerChangeHandlers are included.
*/
abstract class ControllerChangeHandler {
private var forceRemoveViewOnPush = false
/**
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
* In the case that a handler is not reusable, it will be copied using the [.copy] method
* prior to use.
*/
open val isReusable: Boolean = false
/**
* Returns whether or not this handler removes the `from` view from the container when performing a push.
*
* If this is true:
* - This handler's implementation of [performChange] should remove `from` from `container`
* before calling `changeListener.onChangeCompleted()`
* - When a controller is pushed, the previous controller will be detached and its view will be destroyed
*
* If this is false:
* - This handler's implementation of [performChange] should only remove `from` from `container`
* when `isPush` is false
* - When a controller is pushed, the previous controller will stay attached and its view will remain created
* - When a view is recreated (e.g. after a configuration change), any controllers underneath a transaction
* using this handler will have their view recreated and attached, even though they're not the top-most
* controller
*
* If a controller pushed onto the backstack will completely cover the previous controller,
* using a change handler with [removesFromViewOnPush] true should result in no visual interruption
* to the user, while allowing the previous controller's view to be destroyed to reclaim resources.
* If instead, the previous controller should still be visible after the new controller is pushed,
* using a change handler with [removesFromViewOnPush] false will keep the previous controller's
* view in the view hierarchy, where it can still be seen (and even interacted with).
*/
open val removesFromViewOnPush: Boolean = true
private var hasBeenUsed = false
init {
try {
javaClass.getConstructor()
} catch (e: Throwable) {
throw RuntimeException("$javaClass does not have a default constructor.")
}
}
/**
* Responsible for swapping Views from one Controller to another.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or `null` if there was no Controller before this transition
* @param to The next View that should be put in the container or `null` if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param changeListener This listener must be called when any transitions or animations are completed.
*/
abstract fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener,
)
/**
* Saves any data about this handler to a Bundle in case the application is killed.
*
* @param bundle The Bundle into which data should be stored.
*/
open fun saveToBundle(bundle: Bundle) {}
/**
* Restores data that was saved in the [.saveToBundle] method.
*
* @param bundle The bundle that has data to be restored
*/
open fun restoreFromBundle(bundle: Bundle) {}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* popped before it has completed.
*
* @param newHandler The change handler that has caused this push to be aborted
* @param newTop The Controller that will now be at the top of the backstack or `null`
* if there will be no new Controller at the top
*/
open fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* needs to be attached immediately, without any animations or transitions.
*/
open fun completeImmediately() {}
/**
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
* will be saved and restored from the Bundle format.
*/
open fun copy(): ControllerChangeHandler = fromBundle(toBundle())!!
open fun handleOnBackStarted(container: ViewGroup, to: View?, from: View, swipeEdge: Int) {}
open fun handleOnBackProgressed(container: ViewGroup, to: View?, from: View, progress: Float, swipeEdge: Int) {}
open fun handleOnBackCancelled(container: ViewGroup, to: View?, from: View) {}
protected open fun onEnd() {}
fun toBundle(): Bundle {
val bundle = Bundle()
bundle.putString(KEY_CLASS_NAME, javaClass.name)
val savedState = Bundle()
saveToBundle(savedState)
bundle.putBundle(KEY_SAVED_STATE, savedState)
return bundle
}
// Internal modifier plays weirdly with Java, which is what Router is still written in.
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun setForceRemoveViewOnPush(forceRemoveViewOnPush: Boolean) {
this.forceRemoveViewOnPush = forceRemoveViewOnPush
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
interface ControllerChangeListener {
/**
* Called when a [ControllerChangeHandler] has started changing [Controller]s
*
* @param to The new Controller or `null` if no Controller is being transitioned to
* @param from The old Controller or `null` if there was no Controller before this transition
* @param isPush True if this is a push operation, or false if it's a pop.
* @param container The containing ViewGroup
* @param handler The change handler being used.
*/
fun onChangeStarted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler,
)
/**
* Called when a [ControllerChangeHandler] has completed changing [Controller]s
*
* @param to The new Controller or `null` if no Controller is being transitioned to
* @param from The old Controller or `null` if there was no Controller before this transition
* @param isPush True if this was a push operation, or false if it's a pop
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
fun onChangeCompleted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler,
)
}
class ChangeTransaction(
@JvmField val to: Controller?,
@JvmField val from: Controller?,
@JvmField val isPush: Boolean,
@JvmField val container: ViewGroup?,
@JvmField val changeHandler: ControllerChangeHandler?,
@JvmField val listeners: List<ControllerChangeListener>,
)
/**
* A simplified listener for being notified when the change is complete. This MUST be called by any custom
* ControllerChangeHandlers in order to ensure that [Controller]s will be notified of this change.
*/
interface ControllerChangeCompletedListener {
/**
* Called when the change is complete.
*/
fun onChangeCompleted()
}
class ChangeHandlerData(val changeHandler: ControllerChangeHandler, val isPush: Boolean)
companion object {
private const val KEY_CLASS_NAME = "ControllerChangeHandler.className"
private const val KEY_SAVED_STATE = "ControllerChangeHandler.savedState"
val inProgressChangeHandlers: MutableMap<String, ChangeHandlerData> = HashMap()
@JvmStatic
fun fromBundle(bundle: Bundle?): ControllerChangeHandler? {
val bundle = bundle ?: return null
val className = bundle.getString(KEY_CLASS_NAME) ?: return null
val savedState = bundle.getBundle(KEY_SAVED_STATE) ?: return null
return ClassUtils.newInstance<ControllerChangeHandler>(className)?.also {
it.restoreFromBundle(savedState)
}
}
@JvmStatic
fun completeHandlerImmediately(controllerInstanceId: String): Boolean {
inProgressChangeHandlers[controllerInstanceId]?.let { changeHandlerData ->
changeHandlerData.changeHandler.completeImmediately()
inProgressChangeHandlers.remove(controllerInstanceId)
return true
}
return false
}
fun abortOrComplete(toAbort: Controller, newController: Controller?, newChangeHandler: ControllerChangeHandler) {
inProgressChangeHandlers[toAbort.getInstanceId()]?.let { changeHandlerData ->
if (changeHandlerData.isPush) {
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController)
} else {
changeHandlerData.changeHandler.completeImmediately()
}
inProgressChangeHandlers.remove(toAbort.getInstanceId())
}
}
@JvmStatic
fun executeChange(transaction: ChangeTransaction) {
executeChange(
to = transaction.to,
from = transaction.from,
isPush = transaction.isPush,
container = transaction.container,
inHandler = transaction.changeHandler,
listeners = transaction.listeners,
)
}
private fun executeChange(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup?,
inHandler: ControllerChangeHandler?,
listeners: List<ControllerChangeListener>,
) {
container ?: return
val handler: ControllerChangeHandler = if (inHandler == null) {
SimpleSwapChangeHandler()
} else if (inHandler.hasBeenUsed && !inHandler.isReusable) {
inHandler.copy()
} else {
inHandler
}
handler.hasBeenUsed = true
if (from != null) {
if (isPush) {
completeHandlerImmediately(from.getInstanceId())
} else {
abortOrComplete(from, to, handler)
}
}
if (to != null) {
inProgressChangeHandlers[to.getInstanceId()] = ChangeHandlerData(handler, isPush)
}
listeners.forEach { it.onChangeStarted(to, from, isPush, container, handler) }
val toChangeType = if (isPush) ControllerChangeType.PUSH_ENTER else ControllerChangeType.POP_ENTER
val fromChangeType = if (isPush) ControllerChangeType.PUSH_EXIT else ControllerChangeType.POP_EXIT
val toView = to?.let {
it.inflate(container).also {
to.changeStarted(handler, toChangeType)
}
}
val fromView = from?.let {
from.getView().also {
from.changeStarted(handler, fromChangeType)
}
}
handler.performChange(
container = container,
from = fromView,
to = toView,
isPush = isPush,
changeListener = object : ControllerChangeCompletedListener {
override fun onChangeCompleted() {
from?.changeEnded(handler, fromChangeType)
to?.let {
inProgressChangeHandlers.remove(it.getInstanceId())
it.changeEnded(handler, toChangeType)
}
listeners.forEach { it.onChangeCompleted(to, from, isPush, container, handler) }
if (handler.forceRemoveViewOnPush) {
(fromView?.parent as? ViewGroup)?.let {
it.removeView(fromView)
}
}
if (handler.removesFromViewOnPush) {
from?.needsAttach = 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,30 +23,47 @@ 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) {
this.hostId = hostId;
this.tag = tag;
ControllerHostedRouter() {
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
}
final void setHost(@NonNull Controller controller, @NonNull ViewGroup container) {
ControllerHostedRouter(int hostId, @Nullable String tag, boolean boundToContainer) {
this();
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) {
if (hostController == null) {
hostController = controller;
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
}
}
final void setHostContainer(@NonNull Controller controller, @NonNull ViewGroup container) {
if (hostController != controller || this.container != container) {
removeHost();
if (container instanceof ControllerChangeListener) {
addChangeListener((ControllerChangeListener)container);
addChangeListener((ControllerChangeListener) container);
}
hostController = controller;
this.container = container;
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
for (RouterTransaction transaction : backstack) {
transaction.controller().setParentController(controller);
@@ -57,7 +75,7 @@ class ControllerHostedRouter extends Router {
final void removeHost() {
if (container != null && container instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)container);
removeChangeListener((ControllerChangeListener) container);
}
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
@@ -73,7 +91,6 @@ class ControllerHostedRouter extends Router {
}
prepareForContainerRemoval();
hostController = null;
container = null;
}
@@ -108,14 +125,29 @@ class ControllerHostedRouter extends Router {
super.setBackstack(newBackstack, changeHandler);
}
@Override
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
super.performControllerChange(to, from, isPush);
// If we're pushing a transaction that will detach controllers to an unattached child
// router, we need mark all other controllers as NOT needing to be reattached.
if (to != null && !hostController.isAttached()) {
if (to.pushChangeHandler() == null || to.pushChangeHandler().getRemovesFromViewOnPush()) {
for (RouterTransaction transaction : backstack) {
transaction.controller().setNeedsAttach(false);
}
}
}
}
@Override @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
super.onActivityDestroyed(activity, isConfigurationChange);
removeHost();
}
@@ -185,7 +217,7 @@ class ControllerHostedRouter extends Router {
@Override
boolean hasHost() {
return hostController != null;
return hostController != null && container != null;
}
@Override
@@ -193,6 +225,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);
}
@@ -201,22 +234,32 @@ 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);
}
@Override
void setControllerRouter(@NonNull Controller controller) {
void setRouterOnController(@NonNull Controller controller) {
controller.setParentController(hostController);
super.setControllerRouter(controller);
super.setRouterOnController(controller);
}
int getHostId() {
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,15 +1,19 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.ComponentActivity;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -33,20 +37,39 @@ import java.util.List;
*/
public abstract class Router {
private static final String TAG = "Conductor";
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";
private static final String KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED = "Router.onBackPressedDispatcherEnabled";
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;
PopRootControllerMode popRootControllerMode;
boolean onBackPressedDispatcherEnabled;
boolean containerFullyAttached = false;
boolean isActivityStopped = false;
ViewGroup container;
Router() {
backstack.setOnBackstackUpdatedListener(() -> {
if (!onBackPressedDispatcherEnabled) {
return;
}
Iterator<RouterTransaction> iterator = getBackstack().iterator();
int index = 0;
while (iterator.hasNext()) {
iterator.next().controller().onBackPressedCallback.setEnabled(
index++ > 0 || popRootControllerMode != PopRootControllerMode.NEVER
);
}
});
}
/**
* Returns this Router's host Activity or {@code null} if it has either not yet been attached to
* an Activity or if the Activity has been destroyed.
@@ -64,6 +87,22 @@ public abstract class Router {
*/
public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data);
/**
* Returns the OnBackPressedDispatcher for this Router's host Activity or {@code null} if:
* - This Router has not yet been attached to an Activity
* - The attached Activity does not extend ComponentActivity
* - The Activity has been destroyed
*/
@Nullable
public OnBackPressedDispatcher getOnBackPressedDispatcher() {
Activity activity = getActivity();
if (activity instanceof ComponentActivity) {
return ((ComponentActivity) activity).getOnBackPressedDispatcher();
}
return null;
}
/**
* This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded
* to the {@link Controller} with the instanceId passed in.
@@ -84,17 +123,25 @@ public abstract class Router {
* This should be called by the host Activity when its onBackPressed method is called. The call will be forwarded
* to its top {@link Controller}. If that controller doesn't handle it, then it will be popped.
*
* Note: This method has been deprecated and should be replaced with registering OnBackPressedCallbacks with
* Controller instances.
*
* @return Whether or not a back action was handled by the Router
*/
@UiThread
@Deprecated
public boolean handleBack() {
ThreadUtils.ensureMainThread();
return handleBackDispatch();
}
protected boolean handleBackDispatch() {
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller().handleBack()) {
return true;
} else if (popCurrentController()) {
} else if ((backstack.getSize() > 1 || popRootControllerMode != PopRootControllerMode.NEVER) && popCurrentController()) {
return true;
}
}
@@ -140,13 +187,13 @@ public abstract class Router {
RouterTransaction nextTransaction = null;
Iterator<RouterTransaction> iterator = backstack.iterator();
ControllerChangeHandler topPushHandler = topTransaction != null ? topTransaction.pushChangeHandler() : null;
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.removesFromViewOnPush() : false;
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.getRemovesFromViewOnPush() : false;
while (iterator.hasNext()) {
RouterTransaction transaction = iterator.next();
if (transaction.controller() == controller) {
trackDestroyingController(transaction);
iterator.remove();
backstack.remove(transaction);
removedTransaction = transaction;
} else if (removedTransaction != null) {
if (needsNextTransactionAttach && !transaction.controller().isAttached()) {
@@ -161,7 +208,7 @@ public abstract class Router {
}
}
if (popsLastView) {
if (popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW) {
return topTransaction != null;
} else {
return !backstack.isEmpty();
@@ -202,10 +249,10 @@ public abstract class Router {
final ControllerChangeHandler handler = transaction.pushChangeHandler();
if (topTransaction != null) {
//noinspection ConstantConditions
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().getRemovesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.getRemovesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator(), true)) {
performControllerChange(null, visibleTransaction, true, handler);
}
}
@@ -220,12 +267,13 @@ public abstract class Router {
}
void destroy(boolean popViews) {
popsLastView = true;
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW;
final List<RouterTransaction> poppedControllers = backstack.popAll();
trackDestroyingControllers(poppedControllers);
RouterTransaction topTransaction = null;
if (popViews && poppedControllers.size() > 0) {
RouterTransaction topTransaction = poppedControllers.get(0);
topTransaction = poppedControllers.get(0);
topTransaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
@@ -240,6 +288,16 @@ public abstract class Router {
performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler());
}
if (poppedControllers.size() > 0) {
NoOpControllerChangeHandler changeHandler = new NoOpControllerChangeHandler();
for (RouterTransaction routerTransaction : poppedControllers) {
if (routerTransaction != topTransaction) {
routerTransaction.controller().changeStarted(changeHandler, ControllerChangeType.POP_EXIT);
routerTransaction.controller().changeEnded(changeHandler, ControllerChangeType.POP_EXIT);
}
}
}
}
public int getContainerId() {
@@ -250,10 +308,33 @@ 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_BUT_NOT_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;
}
@NonNull
public Router setOnBackPressedDispatcherEnabled(boolean enabled) {
if (backstack.getSize() > 0 && enabled != onBackPressedDispatcherEnabled) {
Log.e(TAG, "setOnBackPressedDispatcherEnabled call ignored, as controllers with a different setting have already been pushed.");
}
onBackPressedDispatcherEnabled = enabled;
return this;
}
@@ -280,7 +361,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 +456,7 @@ public abstract class Router {
*/
@SuppressWarnings("WeakerAccess")
public int getBackstackSize() {
return backstack.size();
return backstack.getSize();
}
/**
@@ -383,7 +464,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());
@@ -404,7 +485,7 @@ public abstract class Router {
ThreadUtils.ensureMainThread();
List<RouterTransaction> oldTransactions = getBackstack();
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator(), false);
removeAllExceptVisibleAndUnowned();
ensureOrderedTransactionIndices(newBackstack);
@@ -434,13 +515,13 @@ public abstract class Router {
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transaction.onAttachedToRouter();
setControllerRouter(transaction.controller());
setRouterOnController(transaction.controller());
}
if (newBackstack.size() > 0) {
List<RouterTransaction> reverseNewBackstack = new ArrayList<>(newBackstack);
Collections.reverse(reverseNewBackstack);
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator());
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator(), false);
boolean newRootRequiresPush = !(newVisibleTransactions.size() > 0 && oldTransactions.contains(newVisibleTransactions.get(0)));
boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions);
@@ -464,7 +545,10 @@ public abstract class Router {
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId());
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
if (transaction.controller().view != null) {
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
}
}
}
@@ -542,14 +626,13 @@ 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 {
setControllerRouter(transaction.controller());
setRouterOnController(transaction.controller());
}
}
}
@@ -605,7 +688,7 @@ public abstract class Router {
isActivityStopped = true;
}
public void onActivityDestroyed(@NonNull Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
prepareForContainerRemoval();
changeListeners.clear();
@@ -613,7 +696,7 @@ public abstract class Router {
transaction.controller().activityDestroyed(activity);
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityDestroyed(activity);
childRouter.onActivityDestroyed(activity, isConfigurationChange);
}
}
@@ -622,7 +705,7 @@ public abstract class Router {
controller.activityDestroyed(activity);
for (Router childRouter : controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
childRouter.onActivityDestroyed(activity, isConfigurationChange);
}
}
@@ -645,19 +728,21 @@ public abstract class Router {
Bundle backstackState = new Bundle();
backstack.saveInstanceState(backstackState);
outState.putInt(KEY_POP_ROOT_CONTROLLER_MODE, popRootControllerMode.ordinal());
outState.putBoolean(KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED, onBackPressedDispatcherEnabled);
outState.putParcelable(KEY_BACKSTACK, backstackState);
outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
//noinspection ConstantConditions
popRootControllerMode = PopRootControllerMode.values()[savedInstanceState.getInt(KEY_POP_ROOT_CONTROLLER_MODE)];
onBackPressedDispatcherEnabled = savedInstanceState.getBoolean(KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED);
backstack.restoreInstanceState(backstackBundle);
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
setControllerRouter(backstackIterator.next().controller());
setRouterOnController(backstackIterator.next().controller());
}
}
@@ -697,7 +782,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<>();
@@ -720,7 +805,13 @@ public abstract class Router {
}
void watchContainerAttach() {
container.post(() -> containerFullyAttached = true);
container.post(new Runnable() {
@Override
public void run() {
containerFullyAttached = true;
performPendingControllerChanges();
}
});
}
void prepareForContainerRemoval() {
@@ -737,9 +828,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()) {
@@ -749,6 +849,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) {
@@ -759,7 +871,7 @@ public abstract class Router {
return null;
}
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
if (isPush && to != null) {
to.onAttachedToRouter();
}
@@ -784,8 +896,8 @@ public abstract class Router {
if (to != null) {
to.ensureValidIndex(getTransactionIndexer());
setControllerRouter(toController);
} else if (backstack.size() == 0 && !popsLastView) {
setRouterOnController(toController);
} 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();
@@ -802,7 +914,7 @@ public abstract class Router {
if (fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
} else {
from.controller().destroy();
fromController.destroy();
}
}
}
@@ -821,7 +933,7 @@ public abstract class Router {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
} else if (from != null && (changeHandler == null || changeHandler.removesFromViewOnPush()) && !containerFullyAttached) {
} else if (from != null && (changeHandler == null || changeHandler.getRemovesFromViewOnPush()) && !containerFullyAttached) {
// If the change handler will remove the from view, we have to make sure the container is fully attached first so we avoid NPEs
// within ViewGroup (details on issue #287). Post this to the container to ensure the attach is complete before we try to remove
// anything.
@@ -829,7 +941,9 @@ public abstract class Router {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
container.post(this::performPendingControllerChanges);
if (container != null) {
container.post(this::performPendingControllerChanges);
}
} else {
ControllerChangeHandler.executeChange(transaction);
}
@@ -873,7 +987,7 @@ public abstract class Router {
private void removeAllExceptVisibleAndUnowned() {
List<View> views = new ArrayList<>();
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator())) {
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator(), false)) {
if (transaction.controller().getView() != null) {
views.add(transaction.controller().getView());
}
@@ -933,14 +1047,20 @@ public abstract class Router {
}
}
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator) {
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator, boolean onlyTop) {
boolean visible = true;
List<RouterTransaction> transactions = new ArrayList<>();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transactions.add(transaction);
//noinspection ConstantConditions
if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) {
if (visible) {
transactions.add(transaction);
}
visible = transaction.pushChangeHandler() != null && !transaction.pushChangeHandler().getRemovesFromViewOnPush();
if (onlyTop && !visible) {
break;
}
}
@@ -963,7 +1083,7 @@ public abstract class Router {
return true;
}
void setControllerRouter(@NonNull Controller controller) {
void setRouterOnController(@NonNull Controller controller) {
controller.setRouter(this);
controller.onContextAvailable();
}
@@ -982,4 +1102,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 is the default 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
}
}
@@ -16,19 +16,18 @@ private const val KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter"
/**
* Metadata used for adding [Controller]s to a [Router].
*/
class RouterTransaction
private constructor(
class RouterTransaction private constructor(
@get:JvmName("controller")
val controller: Controller,
private var tag: String? = null,
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
) {
@RestrictTo(LIBRARY)
internal constructor(bundle: Bundle) : this(
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE)!!),
@@ -1,257 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A base {@link ControllerChangeHandler} that facilitates using {@link android.animation.Animator}s to replace Controller Views
*/
public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
private static final String KEY_DURATION = "AnimatorChangeHandler.duration";
private static final String KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush";
@SuppressWarnings("WeakerAccess")
public static final long DEFAULT_ANIMATION_DURATION = -1;
private long animationDuration;
boolean removesFromViewOnPush;
boolean canceled;
boolean needsImmediateCompletion;
private boolean completed;
Animator animator;
private OnAnimationReadyOrAbortedListener onAnimationReadyOrAbortedListener;
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler() {
this(DEFAULT_ANIMATION_DURATION, true);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(boolean removesFromViewOnPush) {
this(DEFAULT_ANIMATION_DURATION, removesFromViewOnPush);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(long duration) {
this(duration, true);
}
public AnimatorChangeHandler(long duration, boolean removesFromViewOnPush) {
animationDuration = duration;
this.removesFromViewOnPush = removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putLong(KEY_DURATION, animationDuration);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
animationDuration = bundle.getLong(KEY_DURATION);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
if (animator != null) {
animator.cancel();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
@Override
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
if (animator != null) {
animator.end();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
public long getAnimationDuration() {
return animationDuration;
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
/**
* Should be overridden to return the Animator to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
*/
@NonNull
protected abstract Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer);
/**
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
*/
protected abstract void resetFromView(@NonNull View from);
@Override
public final void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
boolean readyToAnimate = true;
final boolean addingToView = to != null && to.getParent() == null;
if (addingToView) {
if (isPush || from == null) {
container.addView(to);
} else if (to.getParent() == null) {
container.addView(to, container.indexOfChild(from));
}
if (to.getWidth() <= 0 && to.getHeight() <= 0) {
readyToAnimate = false;
onAnimationReadyOrAbortedListener = new OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener);
to.getViewTreeObserver().addOnPreDrawListener(onAnimationReadyOrAbortedListener);
}
}
if (readyToAnimate) {
performAnimation(container, from, to, isPush, addingToView, changeListener);
}
}
void complete(@NonNull ControllerChangeCompletedListener changeListener, @Nullable AnimatorListener animatorListener) {
if (!completed) {
completed = true;
changeListener.onChangeCompleted();
}
if (animator != null) {
if (animatorListener != null) {
animator.removeListener(animatorListener);
}
animator.cancel();
animator = null;
}
onAnimationReadyOrAbortedListener = null;
}
void performAnimation(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
complete(changeListener, null);
return;
}
if (needsImmediateCompletion) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
complete(changeListener, null);
if (isPush && from != null) {
resetFromView(from);
}
return;
}
animator = getAnimator(container, from, to, isPush, toAddedToContainer);
if (animationDuration > 0) {
animator.setDuration(animationDuration);
}
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
if (from != null) {
resetFromView(from);
}
if (to != null && to.getParent() == container) {
container.removeView(to);
}
complete(changeListener, this);
}
@Override
public void onAnimationEnd(Animator animation) {
if (!canceled && animator != null) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
complete(changeListener, this);
if (isPush && from != null) {
resetFromView(from);
}
}
}
});
animator.start();
}
private class OnAnimationReadyOrAbortedListener implements ViewTreeObserver.OnPreDrawListener {
@NonNull final ViewGroup container;
@Nullable final View from;
@Nullable final View to;
final boolean isPush;
final boolean addingToView;
@NonNull final ControllerChangeCompletedListener changeListener;
private boolean hasRun;
OnAnimationReadyOrAbortedListener(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean addingToView, @NonNull ControllerChangeCompletedListener changeListener) {
this.container = container;
this.from = from;
this.to = to;
this.isPush = isPush;
this.addingToView = addingToView;
this.changeListener = changeListener;
}
@Override
public boolean onPreDraw() {
onReadyOrAborted();
return true;
}
void onReadyOrAborted() {
if (!hasRun) {
hasRun = true;
if (to != null) {
final ViewTreeObserver observer = to.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
}
performAnimation(container, from, to, isPush, addingToView, changeListener);
}
}
}
}
@@ -0,0 +1,228 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* A base [ControllerChangeHandler] that facilitates using [android.animation.Animator]s to replace Controller Views
*/
abstract class AnimatorChangeHandler @JvmOverloads constructor(
animationDuration: Long = DEFAULT_ANIMATION_DURATION,
removesFromViewOnPush: Boolean = true,
) : ControllerChangeHandler() {
var animationDuration: Long = animationDuration
private set
private var canceled = false
private var needsImmediateCompletion = false
private var completed = false
private var animator: Animator? = null
private var onAnimationReadyOrAbortedListener: OnAnimationReadyOrAbortedListener? = null
private var _removesFromViewOnPush = removesFromViewOnPush
override val removesFromViewOnPush: Boolean
get() = _removesFromViewOnPush
constructor(removesFromViewOnPush: Boolean = true) : this(
animationDuration = DEFAULT_ANIMATION_DURATION,
removesFromViewOnPush = removesFromViewOnPush,
)
override fun saveToBundle(bundle: Bundle) {
super.saveToBundle(bundle)
bundle.putLong(KEY_DURATION, animationDuration)
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush)
}
override fun restoreFromBundle(bundle: Bundle) {
super.restoreFromBundle(bundle)
animationDuration = bundle.getLong(KEY_DURATION)
_removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH)
}
override fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {
super.onAbortPush(newHandler, newTop)
canceled = true
if (animator != null) {
animator!!.cancel()
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener!!.onReadyOrAborted()
}
}
override fun completeImmediately() {
super.completeImmediately()
needsImmediateCompletion = true
if (animator != null) {
animator!!.end()
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener!!.onReadyOrAborted()
}
}
/**
* Should be overridden to return the Animator to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or `null` if there was no Controller before this transition
* @param to The next View that should be put in the container or `null` if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
*/
protected abstract fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator
/**
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
*/
protected abstract fun resetFromView(from: View)
override fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener,
) {
var readyToAnimate = true
val addingToView = to != null && to.parent == null
if (addingToView) {
if (isPush || from == null) {
container.addView(to)
} else if (to!!.parent == null) {
container.addView(to, container.indexOfChild(from))
}
if (to!!.width <= 0 && to.height <= 0) {
readyToAnimate = false
onAnimationReadyOrAbortedListener = OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener)
to.viewTreeObserver.addOnPreDrawListener(onAnimationReadyOrAbortedListener)
}
}
if (readyToAnimate) {
performAnimation(container, from, to, isPush, addingToView, changeListener)
}
}
fun complete(changeListener: ControllerChangeCompletedListener, animatorListener: Animator.AnimatorListener?) {
if (!completed) {
completed = true
changeListener.onChangeCompleted()
}
if (animator != null) {
if (animatorListener != null) {
animator!!.removeListener(animatorListener)
}
animator!!.cancel()
animator = null
}
onAnimationReadyOrAbortedListener = null
}
fun performAnimation(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
changeListener: ControllerChangeCompletedListener,
) {
if (canceled) {
complete(changeListener, null)
return
}
if (needsImmediateCompletion) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from)
}
complete(changeListener, null)
if (isPush && from != null) {
resetFromView(from)
}
return
}
animator = getAnimator(container, from, to, isPush, toAddedToContainer)
if (animationDuration > 0) {
animator!!.duration = animationDuration
}
animator!!.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator) {
from?.let { resetFromView(it) }
if (to != null && to.parent === container) {
container.removeView(to)
}
complete(changeListener, this)
}
override fun onAnimationEnd(animation: Animator) {
if (!canceled && animator != null) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from)
}
complete(changeListener, this)
if (isPush && from != null) {
resetFromView(from)
}
}
}
},
)
animator!!.start()
}
private inner class OnAnimationReadyOrAbortedListener constructor(
val container: ViewGroup,
val from: View?,
val to: View?,
val isPush: Boolean,
val addingToView: Boolean,
val changeListener: ControllerChangeCompletedListener,
) : ViewTreeObserver.OnPreDrawListener {
private var hasRun = false
override fun onPreDraw(): Boolean {
onReadyOrAborted()
return true
}
fun onReadyOrAborted() {
if (!hasRun) {
hasRun = true
if (to != null) {
val observer = to.viewTreeObserver
if (observer.isAlive) {
observer.removeOnPreDrawListener(this)
}
}
performAnimation(container, from, to, isPush, addingToView, changeListener)
}
}
}
companion object {
private const val KEY_DURATION = "AnimatorChangeHandler.duration"
private const val KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush"
const val DEFAULT_ANIMATION_DURATION: Long = -1
}
}
@@ -1,57 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will cross fade two views
*/
public class FadeChangeHandler extends AnimatorChangeHandler {
public FadeChangeHandler() { }
public FadeChangeHandler(boolean removesFromViewOnPush) {
super(removesFromViewOnPush);
}
public FadeChangeHandler(long duration) {
super(duration);
}
public FadeChangeHandler(long duration, boolean removesFromViewOnPush) {
super(duration, removesFromViewOnPush);
}
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null && (!isPush || removesFromViewOnPush())) {
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0));
}
return animator;
}
@Override
protected void resetFromView(@NonNull View from) {
from.setAlpha(1);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new FadeChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -0,0 +1,42 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* An [AnimatorChangeHandler] that will cross fade two views
*/
class FadeChangeHandler : AnimatorChangeHandler {
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
override fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator {
val animator = AnimatorSet()
if (to != null) {
val start = if (toAddedToContainer) 0F else to.alpha
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f))
}
if (from != null && (!isPush || removesFromViewOnPush)) {
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0f))
}
return animator
}
override fun resetFromView(from: View) {
from.alpha = 1f
}
override fun copy(): ControllerChangeHandler = FadeChangeHandler(animationDuration, removesFromViewOnPush)
}
@@ -1,67 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will slide the views left or right, depending on if it's a push or pop.
*/
public class HorizontalChangeHandler extends AnimatorChangeHandler {
public HorizontalChangeHandler() { }
public HorizontalChangeHandler(boolean removesFromViewOnPush) {
super(removesFromViewOnPush);
}
public HorizontalChangeHandler(long duration) {
super(duration);
}
public HorizontalChangeHandler(long duration, boolean removesFromViewOnPush) {
super(duration, removesFromViewOnPush);
}
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animatorSet = new AnimatorSet();
if (isPush) {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.getWidth()));
}
if (to != null) {
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.getWidth(), 0));
}
} else {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.getWidth()));
}
if (to != null) {
// Allow this to have a nice transition when coming off an aborted push animation
float fromLeft = from != null ? from.getTranslationX() : 0;
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
}
}
return animatorSet;
}
@Override
protected void resetFromView(@NonNull View from) {
from.setTranslationX(0);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new HorizontalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -0,0 +1,52 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* An [AnimatorChangeHandler] that will slide the views left or right, depending on if it's a push or pop.
*/
class HorizontalChangeHandler : AnimatorChangeHandler {
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
override fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator {
val animatorSet = AnimatorSet()
if (isPush) {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.width.toFloat()))
}
if (to != null) {
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.width.toFloat(), 0f))
}
} else {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.width.toFloat()))
}
if (to != null) {
// Allow this to have a nice transition when coming off an aborted push animation
val fromLeft = from?.translationX ?: 0F
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.width, 0f))
}
}
return animatorSet
}
override fun resetFromView(from: View) {
from.translationX = 0f
}
override fun copy(): ControllerChangeHandler = HorizontalChangeHandler(animationDuration, removesFromViewOnPush)
}
@@ -1,115 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A {@link ControllerChangeHandler} that will instantly swap Views with no animations or transitions.
*/
public class SimpleSwapChangeHandler extends ControllerChangeHandler implements OnAttachStateChangeListener {
private static final String KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush";
private boolean removesFromViewOnPush;
private boolean canceled;
private ViewGroup container;
private ControllerChangeCompletedListener changeListener;
public SimpleSwapChangeHandler() {
this(true);
}
public SimpleSwapChangeHandler(boolean removesFromViewOnPush) {
this.removesFromViewOnPush = removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
}
@Override
public void completeImmediately() {
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container.removeOnAttachStateChangeListener(this);
container = null;
}
}
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (!canceled) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
if (container.getWindowToken() != null) {
changeListener.onChangeCompleted();
} else {
this.changeListener = changeListener;
this.container = container;
container.addOnAttachStateChangeListener(this);
}
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void onViewAttachedToWindow(@NonNull View v) {
v.removeOnAttachStateChangeListener(this);
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container = null;
}
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new SimpleSwapChangeHandler(removesFromViewOnPush());
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -0,0 +1,88 @@
package com.bluelinelabs.conductor.changehandler
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* A [ControllerChangeHandler] that will instantly swap Views with no animations or transitions.
*/
class SimpleSwapChangeHandler @JvmOverloads constructor(
removesFromViewOnPush: Boolean = true,
) : ControllerChangeHandler(), View.OnAttachStateChangeListener {
private var _removesFromViewOnPush = removesFromViewOnPush
override val removesFromViewOnPush: Boolean
get() = _removesFromViewOnPush
override val isReusable = true
private var canceled = false
private var container: ViewGroup? = null
private var changeListener: ControllerChangeCompletedListener? = null
override fun saveToBundle(bundle: Bundle) {
super.saveToBundle(bundle)
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush)
}
override fun restoreFromBundle(bundle: Bundle) {
super.restoreFromBundle(bundle)
_removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH)
}
override fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {
super.onAbortPush(newHandler, newTop)
canceled = true
}
override fun completeImmediately() {
changeListener?.onChangeCompleted()
changeListener = null
container?.removeOnAttachStateChangeListener(this)
container = null
}
override fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener,
) {
if (canceled) return
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from)
}
if (to != null && to.parent == null) {
container.addView(to)
}
if (container.windowToken != null) {
changeListener.onChangeCompleted()
} else {
this.changeListener = changeListener
this.container = container
container.addOnAttachStateChangeListener(this)
}
}
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
changeListener?.onChangeCompleted()
changeListener = null
container?.removeOnAttachStateChangeListener(this)
container = null
}
override fun onViewDetachedFromWindow(v: View) = Unit
override fun copy(): ControllerChangeHandler = SimpleSwapChangeHandler(removesFromViewOnPush)
}
private const val KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush"
@@ -1,59 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import java.util.ArrayList;
import java.util.List;
/**
* An {@link AnimatorChangeHandler} that will slide either slide a new View up or slide an old View down,
* depending on whether a push or pop change is happening.
*/
public class VerticalChangeHandler extends AnimatorChangeHandler {
public VerticalChangeHandler() { }
public VerticalChangeHandler(boolean removesFromViewOnPush) {
super(removesFromViewOnPush);
}
public VerticalChangeHandler(long duration) {
super(duration);
}
public VerticalChangeHandler(long duration, boolean removesFromViewOnPush) {
super(duration, removesFromViewOnPush);
}
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
List<Animator> viewAnimators = new ArrayList<>();
if (isPush && to != null) {
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.getHeight(), 0));
} else if (!isPush && from != null) {
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.getHeight()));
}
animator.playTogether(viewAnimators);
return animator;
}
@Override
protected void resetFromView(@NonNull View from) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new VerticalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -0,0 +1,43 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* An [AnimatorChangeHandler] that will slide either slide a new View up or slide an old View down,
* depending on whether a push or pop change is happening.
*/
class VerticalChangeHandler : AnimatorChangeHandler {
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
override fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator {
val animator = AnimatorSet()
val viewAnimators: MutableList<Animator> = ArrayList()
if (isPush && to != null) {
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.height.toFloat(), 0f))
} else if (!isPush && from != null) {
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.height.toFloat()))
}
animator.playTogether(viewAnimators)
return animator
}
override fun resetFromView(from: View) = Unit
override fun copy(): ControllerChangeHandler = VerticalChangeHandler(animationDuration, removesFromViewOnPush)
}
@@ -0,0 +1,53 @@
package com.bluelinelabs.conductor.internal
import android.content.Context
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Controller.LifecycleListener
class ControllerLifecycleOwner(lifecycleController: Controller) : LifecycleOwner {
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) // --> State.INITIALIZED
override val lifecycle: Lifecycle
get() = lifecycleRegistry
init {
lifecycleController.addLifecycleListener(
object : LifecycleListener() {
override fun postContextAvailable(controller: Controller, context: Context) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) // --> State.CREATED;
}
override fun postCreateView(controller: Controller, view: View) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) // --> State.STARTED;
}
override fun postAttach(controller: Controller, view: View) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) // --> State.RESUMED;
}
override fun preDetach(controller: Controller, view: View) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) // --> State.STARTED;
}
override fun preDestroyView(controller: Controller, view: View) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) // --> State.CREATED;
}
override fun preContextUnavailable(controller: Controller, context: Context) {
// do nothing
}
override fun preDestroy(controller: Controller) {
// Only act on Controllers that have had at least the onContextAvailable call made on them.
if (lifecycleRegistry.currentState != Lifecycle.State.INITIALIZED) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) // --> State.DESTROYED;
}
}
},
)
}
}
@@ -1,441 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.ActivityHostedRouter;
import com.bluelinelabs.conductor.Router;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LifecycleHandler extends Fragment implements ActivityLifecycleCallbacks {
private static final String FRAGMENT_TAG = "LifecycleHandler";
private static final String KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests";
private static final String KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests";
private static final String KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests";
private static final String KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState";
private Activity activity;
private boolean hasRegisteredCallbacks;
private boolean destroyed;
private boolean attached;
private boolean hasPreparedForHostDetach;
private static final Map<Activity, LifecycleHandler> activeLifecycleHandlers = new HashMap<>();
private SparseArray<String> permissionRequestMap = new SparseArray<>();
private SparseArray<String> activityRequestMap = new SparseArray<>();
private ArrayList<PendingPermissionRequest> pendingPermissionRequests = new ArrayList<>();
private final Map<Integer, ActivityHostedRouter> routerMap = new HashMap<>();
public LifecycleHandler() {
setRetainInstance(true);
setHasOptionsMenu(true);
}
@Nullable
private static LifecycleHandler findInActivity(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = activeLifecycleHandlers.get(activity);
if (lifecycleHandler == null) {
lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
}
if (lifecycleHandler != null) {
lifecycleHandler.registerActivityListener(activity);
}
return lifecycleHandler;
}
@NonNull
public static LifecycleHandler install(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = findInActivity(activity);
if (lifecycleHandler == null) {
lifecycleHandler = new LifecycleHandler();
activity.getFragmentManager().beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit();
}
lifecycleHandler.registerActivityListener(activity);
return lifecycleHandler;
}
@NonNull
public Router getRouter(@NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ActivityHostedRouter router = routerMap.get(getRouterHashKey(container));
if (router == null) {
router = new ActivityHostedRouter();
router.setHost(this, container);
if (savedInstanceState != null) {
Bundle routerSavedState = savedInstanceState.getBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId());
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
routerMap.put(getRouterHashKey(container), router);
} else {
router.setHost(this, container);
}
return router;
}
@NonNull
public List<Router> getRouters() {
return new ArrayList<>(routerMap.values());
}
@Nullable
public Activity getLifecycleActivity() {
return activity;
}
private static int getRouterHashKey(@NonNull ViewGroup viewGroup) {
return viewGroup.getId();
}
private void registerActivityListener(@NonNull Activity activity) {
this.activity = activity;
if (!hasRegisteredCallbacks) {
hasRegisteredCallbacks = true;
activity.getApplication().registerActivityLifecycleCallbacks(this);
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
// trying to immediately get > 1 router in the same Activity. See issue #299.
activeLifecycleHandlers.put(activity, this);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<>();
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<>();
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<>();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(permissionRequestMap));
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(activityRequestMap));
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, pendingPermissionRequests);
}
@Override
public void onDestroy() {
super.onDestroy();
if (activity != null) {
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
activeLifecycleHandlers.remove(activity);
destroyRouters();
activity = null;
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
destroyed = false;
setAttached();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
destroyed = false;
setAttached();
}
@Override
public void onDetach() {
super.onDetach();
attached = false;
destroyRouters();
}
private void setAttached() {
if (!attached) {
attached = true;
for (int i = pendingPermissionRequests.size() - 1; i >= 0; i--) {
PendingPermissionRequest request = pendingPermissionRequests.remove(i);
requestPermissions(request.instanceId, request.permissions, request.requestCode);
}
}
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
}
}
private void destroyRouters() {
if (!destroyed) {
destroyed = true;
if (activity != null) {
for (Router router : getRouters()) {
router.onActivityDestroyed(activity);
}
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
String instanceId = activityRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : getRouters()) {
router.onActivityResult(instanceId, requestCode, resultCode, data);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
String instanceId = permissionRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : getRouters()) {
router.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults);
}
}
}
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
for (Router router : getRouters()) {
Boolean handled = router.handleRequestedPermission(permission);
if (handled != null) {
return handled;
}
}
return super.shouldShowRequestPermissionRationale(permission);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
for (Router router : getRouters()) {
router.onCreateOptionsMenu(menu, inflater);
}
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
for (Router router : getRouters()) {
router.onPrepareOptionsMenu(menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
for (Router router : getRouters()) {
if (router.onOptionsItemSelected(item)) {
return true;
}
}
return super.onOptionsItemSelected(item);
}
public void registerForActivityResult(@NonNull String instanceId, int requestCode) {
activityRequestMap.put(requestCode, instanceId);
}
public void unregisterForActivityResults(@NonNull String instanceId) {
for (int i = activityRequestMap.size() - 1; i >= 0; i--) {
if (instanceId.equals(activityRequestMap.get(activityRequestMap.keyAt(i)))) {
activityRequestMap.removeAt(i);
}
}
}
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode);
}
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode, options);
}
@TargetApi(Build.VERSION_CODES.N)
public void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode,
@Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
@Nullable Bundle options) throws IntentSender.SendIntentException {
registerForActivityResult(instanceId, requestCode);
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
}
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (attached) {
permissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
} else {
pendingPermissionRequests.add(new PendingPermissionRequest(instanceId, permissions, requestCode));
}
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (this.activity == null && findInActivity(activity) == LifecycleHandler.this) {
this.activity = activity;
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
}
}
}
@Override
public void onActivityStarted(Activity activity) {
if (this.activity == activity) {
hasPreparedForHostDetach = false;
for (Router router : getRouters()) {
router.onActivityStarted(activity);
}
}
}
@Override
public void onActivityResumed(Activity activity) {
if (this.activity == activity) {
for (Router router : getRouters()) {
router.onActivityResumed(activity);
}
}
}
@Override
public void onActivityPaused(Activity activity) {
if (this.activity == activity) {
for (Router router : getRouters()) {
router.onActivityPaused(activity);
}
}
}
@Override
public void onActivityStopped(Activity activity) {
if (this.activity == activity) {
prepareForHostDetachIfNeeded();
for (Router router : getRouters()) {
router.onActivityStopped(activity);
}
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
if (this.activity == activity) {
prepareForHostDetachIfNeeded();
for (Router router : getRouters()) {
Bundle bundle = new Bundle();
router.saveInstanceState(bundle);
outState.putBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId(), bundle);
}
}
}
@Override
public void onActivityDestroyed(Activity activity) {
activeLifecycleHandlers.remove(activity);
}
private void prepareForHostDetachIfNeeded() {
if (!hasPreparedForHostDetach) {
hasPreparedForHostDetach = true;
for (Router router : getRouters()) {
router.prepareForHostDetach();
}
}
}
private static class PendingPermissionRequest implements Parcelable {
final String instanceId;
final String[] permissions;
final int requestCode;
PendingPermissionRequest(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
this.instanceId = instanceId;
this.permissions = permissions;
this.requestCode = requestCode;
}
PendingPermissionRequest(Parcel in) {
instanceId = in.readString();
permissions = in.createStringArray();
requestCode = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(instanceId);
out.writeStringArray(permissions);
out.writeInt(requestCode);
}
public static final Parcelable.Creator<PendingPermissionRequest> CREATOR = new Parcelable.Creator<PendingPermissionRequest>() {
@Override
public PendingPermissionRequest createFromParcel(Parcel in) {
return new PendingPermissionRequest(in);
}
@Override
public PendingPermissionRequest[] newArray(int size) {
return new PendingPermissionRequest[size];
}
};
}
}
@@ -0,0 +1,605 @@
package com.bluelinelabs.conductor.internal
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.fragment.app.FragmentActivity
import com.bluelinelabs.conductor.ActivityHostedRouter
import com.bluelinelabs.conductor.Router
import kotlinx.parcelize.Parcelize
internal interface LifecycleHandler {
val routers: List<Router>
val lifecycleActivity: Activity?
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router
fun registerActivityListener(activity: Activity)
fun registerForActivityResult(instanceId: String, requestCode: Int)
fun unregisterForActivityResults(instanceId: String)
fun startActivity(intent: Intent?)
fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle? = null)
@Throws(IntentSender.SendIntentException::class)
fun startIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
)
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int)
companion object {
fun install(activity: Activity, allowAndroidXBacking: Boolean = true): LifecycleHandler {
var lifecycleHandler = findInActivity(activity, allowAndroidXBacking)
if (lifecycleHandler == null) {
if (allowAndroidXBacking && activity is FragmentActivity) {
lifecycleHandler = AndroidXLifecycleHandlerImpl()
activity.supportFragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
} else {
lifecycleHandler = PlatformLifecycleHandlerImpl()
@Suppress("DEPRECATION")
activity.fragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
}
}
lifecycleHandler.registerActivityListener(activity)
return lifecycleHandler
}
}
}
internal class AndroidXLifecycleHandlerImpl : androidx.fragment.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = true)
override val routers: List<Router>
get() = data.routerMap.values.toList()
override val lifecycleActivity: Activity?
get() = data.activity
init {
@Suppress("DEPRECATION")
retainInstance = true
setHasOptionsMenu(true)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleOnCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
handleOnSaveInstanceState(outState)
}
override fun onAttach(context: Context) {
super.onAttach(context)
handleOnAttach(context)
}
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
handleOnActivityResult(requestCode, resultCode, data)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
return handleShouldShowRequestPermissionRationale(permission) {
super.shouldShowRequestPermissionRationale(permission)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
handleOnCreateOptionsMenu(menu, inflater)
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
handleOnPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return handleOnOptionsItemSelected(item) {
super.onOptionsItemSelected(item)
}
}
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
return getRouter(container, savedInstanceState, this)
}
override fun registerActivityListener(activity: Activity) {
handleRegisterActivityListener(activity, this)
}
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
handleRegisterForActivityResult(instanceId, requestCode)
}
override fun unregisterForActivityResults(instanceId: String) {
handleUnregisterForActivityResults(instanceId)
}
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
handleStartActivityForResult(instanceId, intent, requestCode, options)
}
@Suppress("DEPRECATION")
override fun startIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
) {
handleStartIntentSenderForResult(
instanceId = instanceId,
intent = intent,
requestCode = requestCode,
fillInIntent = fillInIntent,
flagsMask = flagsMask,
flagsValues = flagsValues,
extraFlags = extraFlags,
options = options,
) {
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
}
}
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
handleRequestPermissions(instanceId, permissions, requestCode)
}
override fun onDetach() {
super.onDetach()
handleOnDetach()
}
override fun onDestroy() {
super.onDestroy()
handleOnDestroy()
}
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
internal class PlatformLifecycleHandlerImpl : android.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = false)
override val routers: List<Router>
get() = data.routerMap.values.toList()
override val lifecycleActivity: Activity?
get() = data.activity
init {
retainInstance = true
setHasOptionsMenu(true)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleOnCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
handleOnSaveInstanceState(outState)
}
override fun onAttach(activity: Activity) {
super.onAttach(activity)
handleOnAttach(activity)
}
override fun onAttach(context: Context) {
super.onAttach(context)
handleOnAttach(context)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
handleOnActivityResult(requestCode, resultCode, data)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
return handleShouldShowRequestPermissionRationale(permission) {
super.shouldShowRequestPermissionRationale(permission)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
handleOnCreateOptionsMenu(menu, inflater)
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
handleOnPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return handleOnOptionsItemSelected(item) {
super.onOptionsItemSelected(item)
}
}
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
return getRouter(container, savedInstanceState, this)
}
override fun registerActivityListener(activity: Activity) {
handleRegisterActivityListener(activity, this)
}
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
handleRegisterForActivityResult(instanceId, requestCode)
}
override fun unregisterForActivityResults(instanceId: String) {
handleUnregisterForActivityResults(instanceId)
}
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
handleStartActivityForResult(instanceId, intent, requestCode, options)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun startIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
) {
handleStartIntentSenderForResult(
instanceId = instanceId,
intent = intent,
requestCode = requestCode,
fillInIntent = fillInIntent,
flagsMask = flagsMask,
flagsValues = flagsValues,
extraFlags = extraFlags,
options = options,
) {
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
}
}
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
handleRequestPermissions(instanceId, permissions, requestCode)
}
override fun onDetach() {
super.onDetach()
handleOnDetach()
}
override fun onDestroy() {
super.onDestroy()
handleOnDestroy()
}
}
private interface LifecycleHandlerDelegate : ActivityLifecycleCallbacks {
val data: LifecycleHandlerData
private val routers: List<ActivityHostedRouter>
get() = data.routerMap.values.toList()
fun handleOnCreate(savedInstanceState: Bundle?) {
savedInstanceState ?: return
data.permissionRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_PERMISSION_REQUEST_CODES)
?.stringSparseArray
?: SparseArray()
data.activityRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_ACTIVITY_REQUEST_CODES)
?.stringSparseArray
?: SparseArray()
data.pendingPermissionRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS)
?: ArrayList()
}
fun handleOnSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, StringSparseArrayParceler(data.permissionRequestMap))
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, StringSparseArrayParceler(data.activityRequestMap))
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, data.pendingPermissionRequests)
}
fun handleOnDestroy() {
data.activity?.let { activity ->
activity.application.unregisterActivityLifecycleCallbacks(this)
activeLifecycleHandlers.remove(activity)
destroyRouters(false)
data.activity = null
}
data.routerMap.clear()
}
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?, handler: LifecycleHandler): Router {
data.routerMap[routerHashKey(container)]?.let {
it.setHost(handler, container)
return it
}
val router = ActivityHostedRouter()
router.setHost(handler, container)
savedInstanceState?.getBundle("$KEY_ROUTER_STATE_PREFIX${router.containerId}")?.let {
router.restoreInstanceState(it)
}
data.routerMap[routerHashKey(container)] = router
return router
}
fun handleRegisterActivityListener(activity: Activity, handler: LifecycleHandler) {
data.activity = activity
if (!data.hasRegisteredCallbacks) {
data.hasRegisteredCallbacks = true
activity.application.registerActivityLifecycleCallbacks(this)
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
// trying to immediately get > 1 router in the same Activity. See issue #299.
activeLifecycleHandlers[activity] = handler
}
}
fun requestPermissions(permissions: Array<String>, requestCode: Int)
fun handleOnAttach(context: Context) {
if (context is Activity) {
data.activity = context
}
data.destroyed = false
if (!data.attached) {
data.attached = true
for (i in data.pendingPermissionRequests.indices.reversed()) {
val request = data.pendingPermissionRequests.removeAt(i)
handleRequestPermissions(request.instanceId, request.permissions, request.requestCode)
}
routers.forEach { it.onContextAvailable() }
}
}
fun handleOnDetach() {
data.attached = false
data.activity?.let { destroyRouters(it.isChangingConfigurations) }
}
private fun destroyRouters(configurationChange: Boolean) {
if (!data.destroyed) {
data.destroyed = true
data.activity?.let { activity ->
routers.forEach { it.onActivityDestroyed(activity, configurationChange) }
}
}
}
fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
this.data.activityRequestMap[requestCode]?.let { instanceId ->
routers.forEach { it.onActivityResult(instanceId, requestCode, resultCode, data) }
}
}
fun handleOnRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
data.permissionRequestMap[requestCode]?.let { instanceId ->
routers.forEach { it.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults) }
}
}
fun handleShouldShowRequestPermissionRationale(permission: String, callSuper: () -> Boolean): Boolean {
for (router in routers) {
val handled = router.handleRequestedPermission(permission)
if (handled != null) {
return handled
}
}
return callSuper()
}
fun handleOnCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
routers.forEach { it.onCreateOptionsMenu(menu, inflater) }
}
fun handleOnPrepareOptionsMenu(menu: Menu) {
routers.forEach { it.onPrepareOptionsMenu(menu) }
}
fun handleOnOptionsItemSelected(item: MenuItem, callSuper: () -> Boolean): Boolean {
return routers.any { it.onOptionsItemSelected(item) } || callSuper()
}
fun handleRegisterForActivityResult(instanceId: String, requestCode: Int) {
data.activityRequestMap.put(requestCode, instanceId)
}
fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?)
fun handleStartActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
handleRegisterForActivityResult(instanceId, requestCode)
startActivityForResult(intent, requestCode, options)
}
fun handleStartIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
startIntentSender: () -> Unit,
) {
handleRegisterForActivityResult(instanceId, requestCode)
startIntentSender()
}
fun handleUnregisterForActivityResults(instanceId: String) {
for (i in data.activityRequestMap.size() - 1 downTo 0) {
if (instanceId == data.activityRequestMap[data.activityRequestMap.keyAt(i)]) {
data.activityRequestMap.removeAt(i)
}
}
}
fun handleRequestPermissions(
instanceId: String,
permissions: Array<String>,
requestCode: Int,
) {
if (data.attached) {
data.permissionRequestMap.put(requestCode, instanceId)
requestPermissions(permissions, requestCode)
} else {
data.pendingPermissionRequests.add(PendingPermissionRequest(instanceId, permissions, requestCode))
}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (findInActivity(activity, data.isAndroidXLifecycleHandler) === this) {
data.activity = activity
data.routerMap.values.toList().forEach { it.onContextAvailable() }
}
}
override fun onActivityStarted(activity: Activity) {
if (data.activity === activity) {
data.hasPreparedForHostDetach = false
routers.forEach { it.onActivityStarted(activity) }
}
}
override fun onActivityResumed(activity: Activity) {
if (data.activity === activity) {
routers.forEach { it.onActivityResumed(activity) }
}
}
override fun onActivityPaused(activity: Activity) {
if (data.activity === activity) {
routers.forEach { it.onActivityPaused(activity) }
}
}
override fun onActivityStopped(activity: Activity) {
if (data.activity === activity) {
prepareForHostDetachIfNeeded()
routers.forEach { it.onActivityStopped(activity) }
}
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
if (data.activity === activity) {
prepareForHostDetachIfNeeded()
routers.forEach {
val bundle = Bundle()
it.saveInstanceState(bundle)
outState.putBundle("$KEY_ROUTER_STATE_PREFIX${it.containerId}", bundle)
}
}
}
override fun onActivityPreDestroyed(activity: Activity) {
if (data.activity === activity && !activity.isChangingConfigurations) {
handleOnDestroy()
}
}
override fun onActivityDestroyed(activity: Activity) {
activeLifecycleHandlers.remove(activity)
}
private fun prepareForHostDetachIfNeeded() {
if (!data.hasPreparedForHostDetach) {
data.hasPreparedForHostDetach = true
routers.forEach { it.prepareForHostDetach() }
}
}
}
@Parcelize
internal class PendingPermissionRequest(
val instanceId: String,
val permissions: Array<String>,
val requestCode: Int,
) : Parcelable
internal class LifecycleHandlerData(
val isAndroidXLifecycleHandler: Boolean,
var activity: Activity? = null,
var hasRegisteredCallbacks: Boolean = false,
var destroyed: Boolean = false,
var attached: Boolean = false,
var hasPreparedForHostDetach: Boolean = false,
var permissionRequestMap: SparseArray<String> = SparseArray(),
var activityRequestMap: SparseArray<String> = SparseArray(),
var pendingPermissionRequests: ArrayList<PendingPermissionRequest> = arrayListOf(),
val routerMap: MutableMap<Int, ActivityHostedRouter> = mutableMapOf(),
)
private fun findInActivity(activity: Activity, allowAndroidXBacking: Boolean): LifecycleHandler? {
var lifecycleHandler = activeLifecycleHandlers[activity]
if (lifecycleHandler == null) {
lifecycleHandler = if (allowAndroidXBacking && activity is FragmentActivity) {
activity.supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
} else {
@Suppress("DEPRECATION")
activity.fragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
}
}
lifecycleHandler?.registerActivityListener(activity)
return lifecycleHandler
}
private fun routerHashKey(viewGroup: ViewGroup) = viewGroup.id
private val activeLifecycleHandlers = mutableMapOf<Activity, LifecycleHandler>()
private const val FRAGMENT_TAG = "LifecycleHandler"
private const val KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests"
private const val KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests"
private const val KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests"
private const val KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState"
@@ -6,6 +6,8 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
class NoOpControllerChangeHandler : ControllerChangeHandler() {
override val isReusable = true
override fun performChange(
container: ViewGroup,
from: View?,
@@ -17,6 +19,4 @@ class NoOpControllerChangeHandler : ControllerChangeHandler() {
}
override fun copy(): ControllerChangeHandler = NoOpControllerChangeHandler()
override fun isReusable() = true
}
@@ -0,0 +1,249 @@
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.setViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
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
override val lifecycle: LifecycleRegistry
get() = lifecycleRegistry
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
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
) {
view.setViewTreeLifecycleOwner(this@OwnViewTreeLifecycleAndRegistry)
view.setViewTreeSavedStateRegistryOwner(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,
)
GlobalChangeStartListener.onChangeStart(changeController, changeHandler, 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) {
listenForAncestorChangeStart(controller)
}
override fun preContextUnavailable(controller: Controller, context: Context) {
stopListeningForAncestorChangeStart(controller)
}
})
}
private fun listenForAncestorChangeStart(controller: Controller) {
GlobalChangeStartListener.subscribe(controller, controller.ancestors()) { ancestor, changeHandler, changeType ->
// No-op on the case where we (the child controller) hasn't yet created a View as our parent is being
// changed out.
if (::lifecycleRegistry.isInitialized) {
pauseOnChangeStart(
targetController = ancestor,
changeController = ancestor,
changeHandler = changeHandler,
changeType = changeType,
)
}
}
}
private fun stopListeningForAncestorChangeStart(controller: Controller) {
GlobalChangeStartListener.unsubscribe(controller)
}
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
// does this on init, its detach callbacks get called before ours, which prevents us
// from saving state in onDetach. The if statement in here should detect upcoming
// detachment.
private fun pauseOnChangeStart(
targetController: Controller,
changeController: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType,
) {
if (
targetController === changeController &&
!changeType.isEnter &&
changeHandler.removesFromViewOnPush &&
changeController.view != null &&
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
savedRegistryState = Bundle()
savedStateRegistryController.performSave(savedRegistryState)
hasSavedState = true
}
}
private fun Controller.ancestors(): Collection<String> {
return buildList {
var ancestor = parentController
while (ancestor != null) {
add(ancestor.instanceId)
ancestor = ancestor.parentController
}
}
}
companion object {
private const val KEY_SAVED_STATE = "Registry.savedState"
fun own(target: Controller): OwnViewTreeLifecycleAndRegistry {
return OwnViewTreeLifecycleAndRegistry(target)
}
}
}
// In order to prevent child controllers from having strong references to all of their ancestors, some of which may
// break their connection before the child is made aware, this shared listener is used to call all interested parties
// when a controller begins transitioning.
private object GlobalChangeStartListener {
private val listeners = mutableMapOf<String, Listener>()
fun subscribe(
controller: Controller,
targetControllers: Collection<String>,
listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
) {
listeners[controller.instanceId] = Listener(targetControllers, listener)
}
fun unsubscribe(controller: Controller) {
listeners.remove(controller.instanceId)
}
fun onChangeStart(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
listeners.values.forEach { it.call(controller, changeHandler, changeType) }
}
private class Listener(
private val targetControllers: Collection<String>,
private val listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
) {
fun call(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
if (targetControllers.contains(controller.instanceId)) {
listener(controller, changeHandler, changeType)
}
}
}
}
@@ -1,62 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import android.util.SparseArray;
public class StringSparseArrayParceler implements Parcelable {
private final SparseArray<String> stringSparseArray;
public StringSparseArrayParceler(@NonNull SparseArray<String> stringSparseArray) {
this.stringSparseArray = stringSparseArray;
}
StringSparseArrayParceler(@NonNull Parcel in) {
stringSparseArray = new SparseArray<>();
final int size = in.readInt();
for (int i = 0; i < size; i++) {
stringSparseArray.put(in.readInt(), in.readString());
}
}
@NonNull
public SparseArray<String> getStringSparseArray() {
return stringSparseArray;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
final int size = stringSparseArray.size();
out.writeInt(size);
for (int i = 0; i < size; i++) {
int key = stringSparseArray.keyAt(i);
out.writeInt(key);
out.writeString(stringSparseArray.get(key));
}
}
public static final Parcelable.Creator<StringSparseArrayParceler> CREATOR = new Parcelable.Creator<StringSparseArrayParceler>() {
@Override
public StringSparseArrayParceler createFromParcel(Parcel in) {
return new StringSparseArrayParceler(in);
}
@Override
public StringSparseArrayParceler[] newArray(int size) {
return new StringSparseArrayParceler[size];
}
};
}
@@ -0,0 +1,39 @@
package com.bluelinelabs.conductor.internal
import android.os.Parcel
import android.os.Parcelable
import android.util.SparseArray
internal class StringSparseArrayParceler(val stringSparseArray: SparseArray<String>) : Parcelable {
override fun describeContents(): Int = 0
override fun writeToParcel(out: Parcel, flags: Int) {
val size = stringSparseArray.size()
out.writeInt(size)
for (i in 0 until size) {
val key = stringSparseArray.keyAt(i)
out.writeInt(key)
out.writeString(stringSparseArray[key])
}
}
companion object {
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<StringSparseArrayParceler> =
object : Parcelable.Creator<StringSparseArrayParceler> {
override fun createFromParcel(parcel: Parcel): StringSparseArrayParceler {
val stringSparseArray = SparseArray<String>()
val size = parcel.readInt()
for (i in 0 until size) {
stringSparseArray.put(parcel.readInt(), parcel.readString())
}
return StringSparseArrayParceler(stringSparseArray)
}
override fun newArray(size: Int): Array<StringSparseArrayParceler?> = arrayOfNulls(size)
}
}
}
@@ -40,10 +40,12 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
}
rootAttached = true;
listenForDeepestChildAttach(v, () -> {
childrenAttached = true;
reportAttached();
listenForDeepestChildAttach(v, new ChildAttachListener() {
@Override
public void onAttached() {
childrenAttached = true;
reportAttached();
}
});
}
@@ -65,6 +67,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
if (childOnAttachStateChangeListener != null && view instanceof ViewGroup) {
findDeepestChild((ViewGroup)view).removeOnAttachStateChangeListener(childOnAttachStateChangeListener);
childOnAttachStateChangeListener = null;
}
}
@@ -118,7 +121,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
@Override
public void onViewAttachedToWindow(View v) {
if (!attached) {
if (!attached && childOnAttachStateChangeListener != null) {
attached = true;
attachListener.onAttached();
v.removeOnAttachStateChangeListener(this);
@@ -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())
}
}
@@ -1,15 +1,14 @@
package com.bluelinelabs.conductor;
import static org.junit.Assert.assertEquals;
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;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
public class ControllerChangeHandlerTests {
@@ -33,10 +32,10 @@ public class ControllerChangeHandlerTests {
FadeChangeHandler restoredFadeCast = (FadeChangeHandler) restoredFade;
assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
assertEquals(horizontalChangeHandler.getRemovesFromViewOnPush(), restoredHorizontalCast.getRemovesFromViewOnPush());
assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
assertEquals(fadeChangeHandler.removesFromViewOnPush(), restoredFadeCast.removesFromViewOnPush());
assertEquals(fadeChangeHandler.getRemovesFromViewOnPush(), restoredFadeCast.getRemovesFromViewOnPush());
}
}
@@ -1,269 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.View;
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(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(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(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(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 implements 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, 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, 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,716 +0,0 @@
package com.bluelinelabs.conductor;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
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());
}
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,704 @@
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.changeStartCalls++
expectedCallState.changeEndCalls++
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,513 @@
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
controller.requestPermissions(requestedPermissions, 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
child.requestPermissions(requestedPermissions, 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)
}
}

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