From 52d49cbd64be48454ba5ba2abd7761045256c937 Mon Sep 17 00:00:00 2001 From: Niek Haarman Date: Mon, 4 Mar 2019 08:49:10 +0100 Subject: [PATCH 1/2] Set up ext-acorn-experimental module This allows experimental features to be placed into a separate artifact. --- .idea/inspectionProfiles/ktlint.xml | 1 + ext/acorn/acorn-experimental/build.gradle | 28 +++++++++++++++++++ .../acorn-experimental/gradle.properties | 2 ++ settings.gradle | 8 +++++- 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 ext/acorn/acorn-experimental/build.gradle create mode 100644 ext/acorn/acorn-experimental/gradle.properties diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml index be624a3c..551859cf 100644 --- a/.idea/inspectionProfiles/ktlint.xml +++ b/.idea/inspectionProfiles/ktlint.xml @@ -7,5 +7,6 @@ + \ No newline at end of file diff --git a/ext/acorn/acorn-experimental/build.gradle b/ext/acorn/acorn-experimental/build.gradle new file mode 100644 index 00000000..4608aa13 --- /dev/null +++ b/ext/acorn/acorn-experimental/build.gradle @@ -0,0 +1,28 @@ +plugins { + id("org.jetbrains.kotlin.jvm") + id("org.gradle.java-library") + + id("org.jetbrains.dokka") + id("org.gradle.maven-publish") + id("signing") +} + +dependencies { + api project(":acorn") + api project(":ext-acorn") + + implementation "org.jetbrains.kotlin:kotlin-stdlib" + + compileOnly "androidx.annotation:annotation" + + testImplementation "com.nhaarman:expect.kt" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin" + testImplementation "org.junit.jupiter:junit-jupiter-api" + testRuntime "org.junit.jupiter:junit-jupiter-engine" +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs = ["-Xuse-experimental=kotlin.Experimental"] + } +} diff --git a/ext/acorn/acorn-experimental/gradle.properties b/ext/acorn/acorn-experimental/gradle.properties new file mode 100644 index 00000000..7e76c3fa --- /dev/null +++ b/ext/acorn/acorn-experimental/gradle.properties @@ -0,0 +1,2 @@ +groupId=com.nhaarman.acorn.ext +artifactId=acorn-experimental diff --git a/settings.gradle b/settings.gradle index fe51081f..207181dc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,9 @@ include(":ext-acorn-android-lint") include(":ext-acorn-android-timber") include(":ext-acorn-android-appcompat") include(":ext-acorn-android-lifecycle") +include(":ext-acorn-android-statechecks") + +include(":ext-acorn-experimental") include(":samples:hello-world") include(":samples:hello-navigation") @@ -38,5 +41,8 @@ project(":ext-acorn-android-lint").projectDir = file("ext/acorn-android/acorn-an project(":ext-acorn-android-timber").projectDir = file("ext/acorn-android/acorn-android-timber") project(":ext-acorn-android-appcompat").projectDir = file("ext/acorn-android/acorn-android-appcompat") project(":ext-acorn-android-lifecycle").projectDir = file("ext/acorn-android/acorn-android-lifecycle") +project(":ext-acorn-android-statechecks").projectDir = file("ext/acorn-android/acorn-android-statechecks") + +project(":ext-acorn-experimental").projectDir = file("ext/acorn/acorn-experimental") -enableFeaturePreview("STABLE_PUBLISHING") \ No newline at end of file +enableFeaturePreview("STABLE_PUBLISHING") From e40452ee88259bb81eeb93e47105e6fd6400ca0a Mon Sep 17 00:00:00 2001 From: Niek Haarman Date: Tue, 5 Mar 2019 10:32:24 +0100 Subject: [PATCH 2/2] Introduce experimental ConcurrentPairNavigator --- .circleci/test.sh | 9 +- .gitignore | 1 + .idea/inspectionProfiles/ktlint.xml | 1 + .../experimental/CombinedContainer.kt | 52 + .../navigation/experimental/CombinedScene.kt | 58 + .../experimental/ConcurrentPairNavigator.kt | 522 +++++++ .../ExperimentalConcurrentPairNavigator.kt | 29 + .../experimental/internal/Logging.kt | 24 + .../ConcurrentPairNavigatorTest.kt | 1352 +++++++++++++++++ .../experimental/SavableTestScene.kt | 80 + ext/acorn/build.gradle | 2 +- .../acorn/navigation/StackNavigator.kt | 8 +- .../build.gradle | 52 + .../HelloConcurrentPairNavigatorTest.kt | 41 + .../src/main/AndroidManifest.xml | 24 + .../FirstScene.kt | 87 ++ .../FirstSecondTransition.kt | 52 + .../FirstSecondViewController.kt | 39 + .../HelloConcurrentPairNavigator.kt | 54 + .../HelloConcurrentPairNavigatorProvider.kt | 27 + ...rrentPairNavigatorViewControllerFactory.kt | 35 + .../MainActivity.kt | 42 + .../MyApplication.kt | 28 + .../SecondFirstTransition.kt | 71 + .../SecondScene.kt | 51 + .../res/layout/first_and_second_scene.xml | 13 + .../src/main/res/layout/first_scene.xml | 36 + .../src/main/res/layout/second_scene.xml | 56 + .../src/main/res/values/strings.xml | 10 + .../src/main/res/values/styles.xml | 8 + .../FirstSceneTest.kt | 84 + settings.gradle | 5 +- test | 7 +- 33 files changed, 2946 insertions(+), 14 deletions(-) create mode 100644 ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedContainer.kt create mode 100644 ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedScene.kt create mode 100644 ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigator.kt create mode 100644 ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ExperimentalConcurrentPairNavigator.kt create mode 100644 ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/internal/Logging.kt create mode 100644 ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigatorTest.kt create mode 100644 ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/SavableTestScene.kt create mode 100644 samples/hello-concurrentpairnavigator/build.gradle create mode 100644 samples/hello-concurrentpairnavigator/src/androidTest/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorTest.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/AndroidManifest.xml create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstScene.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondTransition.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondViewController.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigator.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorProvider.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorViewControllerFactory.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MainActivity.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MyApplication.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondFirstTransition.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondScene.kt create mode 100644 samples/hello-concurrentpairnavigator/src/main/res/layout/first_and_second_scene.xml create mode 100644 samples/hello-concurrentpairnavigator/src/main/res/layout/first_scene.xml create mode 100644 samples/hello-concurrentpairnavigator/src/main/res/layout/second_scene.xml create mode 100644 samples/hello-concurrentpairnavigator/src/main/res/values/strings.xml create mode 100644 samples/hello-concurrentpairnavigator/src/main/res/values/styles.xml create mode 100644 samples/hello-concurrentpairnavigator/src/test/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSceneTest.kt diff --git a/.circleci/test.sh b/.circleci/test.sh index 5d586a39..fabb76eb 100755 --- a/.circleci/test.sh +++ b/.circleci/test.sh @@ -8,13 +8,14 @@ :ext-acorn-android-timber:lint \ :ext-acorn-android-lifecycle:lint \ \ - :samples:hello-world:lintRelease \ + :samples:hello-concurrentpairnavigator:lintRelease \ :samples:hello-navigation:lintRelease \ - :samples:hello-staterestoration:lintRelease \ - :samples:hello-startactivity:lintRelease \ :samples:hello-sharedata:lintRelease \ - :samples:hello-viewfactory:lintRelease \ + :samples:hello-startactivity:lintRelease \ + :samples:hello-staterestoration:lintRelease \ :samples:hello-transitionanimation:lintRelease \ + :samples:hello-viewfactory:lintRelease \ + :samples:hello-world:lintRelease \ \ :samples:notes-app:android:lintRelease \ \ diff --git a/.gitignore b/.gitignore index a5a9fcc9..86cc062f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ **.log *.orig +*.attach_* .idea/assetWizardSettings.xml .idea/caches/* diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml index 551859cf..97ef411e 100644 --- a/.idea/inspectionProfiles/ktlint.xml +++ b/.idea/inspectionProfiles/ktlint.xml @@ -7,6 +7,7 @@ + \ No newline at end of file diff --git a/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedContainer.kt b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedContainer.kt new file mode 100644 index 00000000..ab8608a2 --- /dev/null +++ b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedContainer.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.navigation.experimental + +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.RestorableContainer +import com.nhaarman.acorn.presentation.SavableContainer +import com.nhaarman.acorn.state.ContainerState +import com.nhaarman.acorn.state.get + +/** + * A [Container] that combines two Containers into one. + * + * This interface is used in conjunction with [ConcurrentPairNavigator]. + */ +@ExperimentalConcurrentPairNavigator +interface CombinedContainer : RestorableContainer { + + val firstContainer: Container + val secondContainer: Container + + override fun saveInstanceState(): ContainerState { + return ContainerState().also { + it["first"] = (firstContainer as? SavableContainer)?.saveInstanceState() + it["second"] = (secondContainer as? SavableContainer)?.saveInstanceState() + } + } + + override fun restoreInstanceState(bundle: ContainerState) { + bundle.get("first")?.let { + (firstContainer as? RestorableContainer)?.restoreInstanceState(it) + } + + bundle.get("second")?.let { + (secondContainer as? RestorableContainer)?.restoreInstanceState(it) + } + } +} diff --git a/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedScene.kt b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedScene.kt new file mode 100644 index 00000000..7ac75f00 --- /dev/null +++ b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/CombinedScene.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.navigation.experimental + +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.Scene +import com.nhaarman.acorn.presentation.SceneKey + +/** + * A [Scene] that combines two Scenes into one. + * The key of this Scene is taken from [secondScene]. + * + * This class is used in conjunction with [ConcurrentPairNavigator]. + */ +@ExperimentalConcurrentPairNavigator +class CombinedScene( + val firstScene: Scene, + val secondScene: Scene +) : Scene { + + override val key: SceneKey + get() = secondScene.key + + override fun onStart() { + } + + @Suppress("UNCHECKED_CAST") + override fun attach(v: CombinedContainer) { + (firstScene as Scene).attach(v.firstContainer) + (secondScene as Scene).attach(v.secondContainer) + } + + @Suppress("UNCHECKED_CAST") + override fun detach(v: CombinedContainer) { + (secondScene as Scene).detach(v.secondContainer) + (firstScene as Scene).detach(v.firstContainer) + } + + override fun onStop() { + } + + override fun onDestroy() { + } +} diff --git a/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigator.kt b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigator.kt new file mode 100644 index 00000000..6318e680 --- /dev/null +++ b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigator.kt @@ -0,0 +1,522 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.navigation.experimental + +import androidx.annotation.CallSuper +import com.nhaarman.acorn.OnBackPressListener +import com.nhaarman.acorn.navigation.DisposableHandle +import com.nhaarman.acorn.navigation.Navigator +import com.nhaarman.acorn.navigation.SavableNavigator +import com.nhaarman.acorn.navigation.TransitionData +import com.nhaarman.acorn.navigation.experimental.internal.v +import com.nhaarman.acorn.navigation.experimental.internal.w +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.SavableScene +import com.nhaarman.acorn.presentation.Scene +import com.nhaarman.acorn.state.NavigatorState +import com.nhaarman.acorn.state.SceneState +import com.nhaarman.acorn.state.get +import com.nhaarman.acorn.state.navigatorState +import com.nhaarman.acorn.util.lazyVar +import kotlin.reflect.KClass + +/** + * An abstract [Navigator] class that supports a second, overlapping [Scene]. + * + * This Navigator supports two states: one where only the initial Scene is + * active, and one where both the initial Scene and an additional Scene are + * active. When adding the second Scene using [push], both the initial and the + * additional Scene will be in their 'started' state. This can be useful when + * showing a sophisticated modal kind of overlay. + * + * When the additional Scene is pushed, this Navigator wraps both the initial + * Scene and the second Scene in a [CombinedScene] and notifies interested + * [Navigator.Events] of this CombinedScene. + * + * This Navigator implements [SavableNavigator] and thus can have its state + * saved and restored when necessary. + * + * @param savedState An optional instance that contains the saved state as + * returned by this class's [saveInstanceState] method. + */ +@ExperimentalConcurrentPairNavigator +abstract class ConcurrentPairNavigator( + private val savedState: NavigatorState? +) : Navigator, SavableNavigator, OnBackPressListener { + + /** + * Creates the initial [Scene] for this ConcurrentPairNavigator. + */ + protected abstract fun createInitialScene(): Scene + + /** + * Instantiates a [Scene] instance for given [sceneClass] and [state]. + * + * This function is called when restoring the ConcurrentPairNavigator from a + * saved state. + * + * @param sceneClass The class of the [Scene] to instantiate. + * @param state The saved state of the [Scene] if applicable. This will be + * the instance as returned from [SavableScene.saveInstanceState] if its + * state was saved. + */ + protected abstract fun instantiateScene(sceneClass: KClass>, state: SceneState?): Scene + + private var state: State by lazyVar { + + @Suppress("UNCHECKED_CAST") + fun baseScene(savedState: NavigatorState): Scene? { + val className = savedState.get("base_class") ?: return null + val baseSceneClass = Class.forName(className).kotlin as? KClass> ?: return null + val baseSceneState: SceneState = savedState["base_state"] ?: return null + return instantiateScene(baseSceneClass, baseSceneState) + } + + @Suppress("UNCHECKED_CAST") + fun secondScene(savedState: NavigatorState): Scene? { + val className = savedState.get("second_class") ?: return null + val secondSceneClass = Class.forName(className).kotlin as? KClass> ?: return null + val secondSceneState: SceneState? = savedState["second_state"] ?: return null + return instantiateScene(secondSceneClass, secondSceneState) + } + + fun initialScenes(): Scenes { + if (savedState == null) return Scenes( + createInitialScene() + ) + + val baseScene = baseScene(savedState) ?: return Scenes( + createInitialScene() + ) + val secondScene = secondScene(savedState) + + return Scenes(baseScene, secondScene) + } + + State.create(initialScenes()) + } + + @CallSuper + override fun addNavigatorEventsListener(listener: Navigator.Events): DisposableHandle { + state.addListener(listener) + + return object : DisposableHandle { + + override fun isDisposed(): Boolean { + return listener !in state.listeners + } + + override fun dispose() { + state.removeListener(listener) + } + } + } + + /** + * Sets given [Scene] as the second Scene in this ConcurrentPairNavigator. + * + * If this Navigator is currently active and there is already a second + * Scene active, this second Scene will be stopped and destroyed. Given + * Scene will receive a call to [Scene.onStart]. The initial Scene will + * receive no lifecycle methods. + * + * If this Navigator is currently inactive and there is already a second + * Scene active, this second Scene will be destroyed. No further lifecycle + * methods will be called. Starting this Navigator will start both the + * initial Scene and given second Scene. + * + * Calling this method when this Navigator has been destroyed will have no + * effect. + * + * @param scene The [Scene] instance to push. + */ + fun push(scene: Scene) { + v("ConcurrentPairNavigator", "push $scene") + execute(state.push(scene, TransitionData.forwards)) + } + + /** + * Removes the second [Scene] if there is any. + * + * If there is no second Scene, nothing will happen. + * + * If this Navigator is currently active and there is a second Scene, this + * second Scene will be stopped and destroyed. + * + * If this Navigator is currently inactive and there is a second Scene, this + * second Scene will be destroyed. + * + * Calling this method when the receiving Navigator has been destroyed will + * have no effect. + */ + fun pop() { + execute(state.pop()) + } + + /** + * Finishes this Navigator. + * + * If this Navigator is currently active, any active Scenes will go through + * their destroying lifecycles calling [Scene.onStop] and [Scene.onDestroy]. + * + * If this Navigator is currently not active, any active Scenes will only + * have their [Scene.onDestroy] method called. + * + * Calling this method when the Navigator has been destroyed will have no + * effect. + */ + fun finish() { + execute(state.finish()) + } + + @CallSuper + override fun onStart() { + v("ConcurrentPairNavigator", "onStart") + execute(state.start()) + } + + @CallSuper + override fun onStop() { + v("ConcurrentPairNavigator", "onStop") + execute(state.stop()) + } + + @CallSuper + override fun onDestroy() { + v("ConcurrentPairNavigator", "onDestroy") + execute(state.destroy()) + } + + @CallSuper + override fun onBackPressed(): Boolean { + if (isDestroyed()) return false + + execute(state.onBackPressed()) + return true + } + + private fun execute(transition: StateTransition) { + state = transition.newState + transition.action?.invoke() + } + + override fun isDestroyed(): Boolean { + return state is State.Destroyed + } + + @CallSuper + override fun saveInstanceState(): NavigatorState { + return navigatorState { + it["base_class"] = state.scenes.baseScene::class.java.name + it["base_state"] = (state.scenes.baseScene as? SavableScene)?.saveInstanceState() + + it["second_class"] = state.scenes.secondScene?.let { it::class.java.name } + it["second_state"] = (state.scenes.secondScene as? SavableScene)?.saveInstanceState() + } + } + + private sealed class State { + + abstract val scenes: Scenes + abstract val listeners: List + + abstract fun addListener(listener: Navigator.Events) + abstract fun removeListener(listener: Navigator.Events) + + abstract fun start(): StateTransition + abstract fun stop(): StateTransition + abstract fun destroy(): StateTransition + + abstract fun push(scene: Scene, data: TransitionData?): StateTransition + abstract fun pop(): StateTransition + abstract fun onBackPressed(): StateTransition + + abstract fun finish(): StateTransition + + companion object { + + fun create(initialScenes: Scenes): State { + return Inactive( + initialScenes, + emptyList() + ) + } + } + + class Inactive( + override val scenes: Scenes, + override var listeners: List + ) : State() { + + override fun addListener(listener: Navigator.Events) { + listeners += listener + } + + override fun removeListener(listener: Navigator.Events) { + listeners -= listener + } + + override fun start(): StateTransition { + return StateTransition( + Active( + scenes, + listeners + ) + ) { + scenes.baseScene.onStart() + scenes.secondScene?.onStart() + listeners.forEach { it.scene(scenes.scene(), null) } + } + } + + override fun stop(): StateTransition { + return StateTransition( + Inactive( + scenes, + listeners + ) + ) + } + + override fun destroy(): StateTransition { + return StateTransition(Destroyed()) { + scenes.secondScene?.onDestroy() + scenes.baseScene.onDestroy() + } + } + + override fun push(scene: Scene, data: TransitionData?): StateTransition { + return StateTransition( + Inactive( + Scenes(scenes.baseScene, scene), + listeners + ) + ) { + scenes.secondScene?.onDestroy() + } + } + + override fun pop(): StateTransition { + return StateTransition(this) { + scenes.secondScene?.onDestroy() + } + } + + override fun onBackPressed(): StateTransition { + val poppedScene = scenes.secondScene + if (poppedScene == null) { + return StateTransition(this) { + scenes.baseScene.onDestroy() + listeners.forEach { it.finished() } + } + } + + return StateTransition( + Inactive( + Scenes(scenes.baseScene), + listeners + ) + ) { + poppedScene.onDestroy() + } + } + + override fun finish(): StateTransition { + return StateTransition(Destroyed()) { + scenes.secondScene?.onDestroy() + scenes.baseScene.onDestroy() + listeners.forEach { it.finished() } + } + } + } + + class Active( + override val scenes: Scenes, + override var listeners: List + ) : State() { + + override fun addListener(listener: Navigator.Events) { + listeners += listener + listener.scene(scenes.scene(), null) + } + + override fun removeListener(listener: Navigator.Events) { + listeners -= listener + } + + override fun start(): StateTransition { + return StateTransition(this) + } + + override fun stop(): StateTransition { + return StateTransition( + Inactive( + scenes, + listeners + ) + ) { + scenes.secondScene?.onStop() + scenes.baseScene.onStop() + } + } + + override fun destroy(): StateTransition { + return StateTransition(Destroyed()) { + scenes.secondScene?.onStop() + scenes.baseScene.onStop() + + scenes.secondScene?.onDestroy() + scenes.baseScene.onDestroy() + } + } + + override fun push(scene: Scene, data: TransitionData?): StateTransition { + val newScenes = + Scenes(scenes.baseScene, scene) + return StateTransition( + Active( + newScenes, + listeners + ) + ) { + scenes.secondScene?.onStop() + scenes.secondScene?.onDestroy() + + scene.onStart() + listeners.forEach { it.scene(newScenes.scene(), data) } + } + } + + override fun pop(): StateTransition { + val poppedScene = scenes.secondScene + if (poppedScene == null) { + return StateTransition(this) + } + + return StateTransition( + Active( + Scenes(scenes.baseScene), + listeners + ) + ) { + poppedScene.onStop() + poppedScene.onDestroy() + listeners.forEach { it.scene(scenes.baseScene, TransitionData.backwards) } + } + } + + override fun onBackPressed(): StateTransition { + val poppedScene = scenes.secondScene + if (poppedScene == null) { + return StateTransition(Destroyed()) { + scenes.baseScene.onStop() + scenes.baseScene.onDestroy() + listeners.forEach { it.finished() } + } + } + + return StateTransition( + Active( + Scenes(scenes.baseScene), + listeners + ) + ) { + poppedScene.onStop() + poppedScene.onDestroy() + + listeners.forEach { it.scene(scenes.baseScene) } + } + } + + override fun finish(): StateTransition { + return StateTransition(Destroyed()) { + scenes.secondScene?.onStop() + scenes.baseScene.onStop() + + scenes.secondScene?.onDestroy() + scenes.baseScene.onDestroy() + + listeners.forEach { it.finished() } + } + } + } + + class Destroyed : State() { + + override val scenes: Scenes get() = error("") + override val listeners = emptyList() + + override fun addListener(listener: Navigator.Events) { + } + + override fun removeListener(listener: Navigator.Events) { + } + + override fun start(): StateTransition { + w("ConcurrentPairNavigator.State", "Warning: Cannot start state after navigator is destroyed.") + return StateTransition(this) + } + + override fun stop(): StateTransition { + return StateTransition(this) + } + + override fun destroy(): StateTransition { + return StateTransition(this) + } + + override fun push(scene: Scene, data: TransitionData?): StateTransition { + w("ConcurrentPairNavigator.State", "Warning: Cannot push scene after navigator is destroyed.") + return StateTransition(this) + } + + override fun pop(): StateTransition { + w("ConcurrentPairNavigator.State", "Warning: Cannot pop scene after navigator is destroyed.") + return StateTransition(this) + } + + override fun onBackPressed(): StateTransition { + w("ConcurrentPairNavigator.State", "Warning: Cannot handle onBackPressed after navigator is destroyed.") + return StateTransition(this) + } + + override fun finish(): StateTransition { + w("ConcurrentPairNavigator.State", "Warning: Cannot finish navigator after navigator is destroyed.") + return StateTransition(this) + } + } + } + + private class Scenes( + val baseScene: Scene, + val secondScene: Scene? = null + ) { + + fun scene(): Scene<*> { + if (secondScene != null) { + return CombinedScene(baseScene, secondScene) + } + + return baseScene + } + } + + private class StateTransition( + val newState: State, + val action: (() -> Unit)? = null + ) +} diff --git a/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ExperimentalConcurrentPairNavigator.kt b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ExperimentalConcurrentPairNavigator.kt new file mode 100644 index 00000000..d8372d9b --- /dev/null +++ b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/ExperimentalConcurrentPairNavigator.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.navigation.experimental + +/** + * Marks all [ConcurrentPairNavigator] declarations as an experimental API to + * indicate that it is still experimental. + * + * The ConcurrentPairNavigator has no backward compatibility guarantees + * whatsoever. + */ +@Experimental +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class ExperimentalConcurrentPairNavigator diff --git a/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/internal/Logging.kt b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/internal/Logging.kt new file mode 100644 index 00000000..c82196f4 --- /dev/null +++ b/ext/acorn/acorn-experimental/src/main/java/com/nhaarman/acorn/navigation/experimental/internal/Logging.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.navigation.experimental.internal + +import com.nhaarman.acorn.logger + +internal fun v(tag: String, message: Any?) = logger?.v(tag, message) +internal fun d(tag: String, message: Any?) = logger?.d(tag, message) +internal fun i(tag: String, message: Any?) = logger?.i(tag, message) +internal fun w(tag: String, message: Any?) = logger?.w(tag, message) diff --git a/ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigatorTest.kt b/ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigatorTest.kt new file mode 100644 index 00000000..5dc28c40 --- /dev/null +++ b/ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/ConcurrentPairNavigatorTest.kt @@ -0,0 +1,1352 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.navigation.experimental + +import com.nhaarman.acorn.navigation.Navigator +import com.nhaarman.acorn.navigation.TransitionData +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.Scene +import com.nhaarman.acorn.state.NavigatorState +import com.nhaarman.acorn.state.SavedState +import com.nhaarman.acorn.state.SceneState +import com.nhaarman.expect.expect +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.inOrder +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.spy +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.reflect.KClass + +@ExperimentalConcurrentPairNavigator +internal class ConcurrentPairNavigatorTest { + + private val scene1 = spy(SavableTestScene(1)) + private val scene2 = spy(SavableTestScene(2)) + private val scene3 = spy(SavableTestScene(3)) + + private val navigator = + TestConcurrentPairNavigator(scene1) + private val listener = spy(TestListener()) + + @Nested + inner class NavigatorStates { + + @Test + fun `non disposed listener is not disposed`() { + /* Given */ + val disposable = navigator.addNavigatorEventsListener(listener) + + /* Then */ + expect(disposable.isDisposed()).toBe(false) + } + + @Test + fun `disposed listener is disposed`() { + /* Given */ + val disposable = navigator.addNavigatorEventsListener(listener) + + /* When */ + disposable.dispose() + + /* Then */ + expect(disposable.isDisposed()).toBe(true) + } + + @Nested + inner class InactiveNavigator { + + @Test + fun `inactive navigator is not finished`() { + /* When */ + navigator.addNavigatorEventsListener(listener) + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `inactive navigator is not destroyed`() { + /* Then */ + expect(navigator.isDestroyed()).toBe(false) + } + + @Test + fun `inactive navigator does not notify newly added listener of scene`() { + /* When */ + navigator.addNavigatorEventsListener(listener) + + /* Then */ + expect(listener.lastScene).toBeNull() + } + + @Test + fun `stopping inactive navigator does not finish`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.onStop() + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `destroying inactive navigator does not finish`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.onDestroy() + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `pushing a scene for inactive navigator does not notify listeners`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.push(scene2) + + /* Then */ + expect(listener.lastScene).toBeNull() + } + + @Test + fun `popping for only base scene for inactive navigator does not notify finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `popping for base and second scene for inactive navigator does not notify finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.push(scene2) + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `popping for base and second scene for inactive navigator does not notify scene`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.push(scene2) + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.lastScene).toBeNull() + } + + @Test + fun `finish for inactive navigator notifies listeners of finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.finish() + + /* Then */ + expect(listener.finished).toBe(true) + } + + @Test + fun `onBackPressed for only base scene for inactive navigator notifies listeners of finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + val result = navigator.onBackPressed() + + /* Then */ + expect(result).toBe(true) + expect(listener.finished).toBe(true) + } + + @Test + fun `onBackPressed for base and second scene for inactive navigator does not notify listeners of finished`() { + /* Given */ + navigator.push(scene2) + navigator.addNavigatorEventsListener(listener) + + /* When */ + val result = navigator.onBackPressed() + + /* Then */ + expect(result).toBe(true) + expect(listener.finished).toBe(false) + } + } + + @Nested + inner class StartingNavigator { + + @Test + fun `starting navigator notifies listeners of scene`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.onStart() + + /* Then */ + expect(listener.lastScene).toBe(scene1) + } + + @Test + fun `starting navigator does not notify removed listeners of scene`() { + /* Given */ + val disposable = navigator.addNavigatorEventsListener(listener) + disposable.dispose() + + /* When */ + navigator.onStart() + + /* Then */ + verify(listener, never()).scene(any(), anyOrNull()) + } + + @Test + fun `starting navigator multiple times notifies listeners of scene only once`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.onStart() + navigator.onStart() + + /* Then */ + verify(listener, times(1)).scene(any(), anyOrNull()) + } + + @Test + fun `starting navigator second time in a callback only notifies listener once`() { + /* Given */ + val listener = mock() + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.addNavigatorEventsListener(object : Navigator.Events { + override fun scene(scene: Scene, data: TransitionData?) { + navigator.onStart() + } + + override fun finished() { + } + }) + navigator.onStart() + + /* Then */ + verify(listener, times(1)).scene(any(), anyOrNull()) + } + + @Test + fun `starting navigator - scene notification has no transition data`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.onStart() + + /* Then */ + expect(listener.lastTransitionData).toBeNull() + } + + @Test + fun `starting navigator does not finish`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + + /* When */ + navigator.onStart() + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `start navigator after scene push notifies combined scene`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.push(scene2) + + /* When */ + navigator.onStart() + + /* Then */ + expect(listener.lastScene).toBeInstanceOf { + expect(it.key).toBe(scene2.key) + expect(it.firstScene).toBe(scene1) + expect(it.secondScene).toBe(scene2) + } + } + } + + @Nested + inner class ActiveNavigator { + + @Test + fun `active navigator is not destroyed`() { + /* Given */ + navigator.onStart() + + /* Then */ + expect(navigator.isDestroyed()).toBe(false) + } + + @Test + fun `active navigator does notify newly added listener of scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.addNavigatorEventsListener(listener) + + /* Then */ + expect(listener.lastScene).toBe(scene1) + } + + @Test + fun `active navigator with second scene does notify newly added listener of combined scene`() { + /* Given */ + navigator.push(scene2) + navigator.onStart() + + /* When */ + navigator.addNavigatorEventsListener(listener) + + /* Then */ + expect(listener.lastScene).toBeInstanceOf { + expect(it.key).toBe(scene2.key) + expect(it.firstScene).toBe(scene1) + expect(it.secondScene).toBe(scene2) + } + } + + @Test + fun `pushing a scene for active navigator does notify listeners`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + + /* When */ + navigator.push(scene2) + + /* Then */ + expect(listener.lastScene).toBeInstanceOf { + expect(it.key).toBe(scene2.key) + expect(it.firstScene).toBe(scene1) + expect(it.secondScene).toBe(scene2) + } + } + + @Test + fun `pushing a scene for active navigator does not notify removed listeners`() { + /* Given */ + navigator.onStart() + navigator.addNavigatorEventsListener(listener).dispose() + + /* When */ + navigator.push(scene2) + + /* Then */ + verify(listener, never()).scene(eq(scene2), anyOrNull()) + } + + @Test + fun `pushing a scene for active navigator - scene notification has forward transition data`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + + /* When */ + navigator.push(scene2) + + /* Then */ + expect(listener.lastTransitionData?.isBackwards).toBe(false) + } + + @Test + fun `popping for only base scene for active navigator does not notify finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `popping for base and second scene for active navigator does not notify finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + navigator.push(scene2) + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.finished).toBe(false) + } + + @Test + fun `popping for base and second scene for active navigator notifies proper scene`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + navigator.push(scene2) + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.lastScene).toBe(scene1) + } + + @Test + fun `popping for base and second scene for active navigator - scene notification has backward transition data`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + navigator.push(scene2) + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.lastTransitionData?.isBackwards).toBe(true) + } + + @Test + fun `onBackPressed for only base scene for active navigator notifies listeners of finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + + /* When */ + val result = navigator.onBackPressed() + + /* Then */ + expect(result).toBe(true) + expect(listener.finished).toBe(true) + } + + @Test + fun `onBackPressed for base and second scene for active navigator does not notify listeners of finished`() { + /* Given */ + navigator.push(scene2) + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + + /* When */ + val result = navigator.onBackPressed() + + /* Then */ + expect(result).toBe(true) + expect(listener.finished).toBe(false) + } + + @Test + fun `onBackPressed for base and second scene for active navigator notifies listener of base scene`() { + /* Given */ + navigator.push(scene2) + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + + /* When */ + navigator.onBackPressed() + + /* Then */ + expect(listener.lastScene).toBe(scene1) + } + + @Test + fun `onBackPressed for only base scene for active navigator results in destroyed navigator`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.onBackPressed() + + /* Then */ + expect(navigator.isDestroyed()).toBe(true) + } + + @Test + fun `finish for active navigator notifies listeners of finished`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + + /* When */ + navigator.finish() + + /* Then */ + expect(listener.finished).toBe(true) + } + + @Test + fun `finish for active navigator results in destroyed navigator`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.finish() + + /* Then */ + expect(navigator.isDestroyed()).toBe(true) + } + } + + @Nested + inner class StoppedNavigator { + + @Test + fun `stopped navigator is not destroyed`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.onStop() + + /* Then */ + expect(navigator.isDestroyed()).toBe(false) + } + } + + @Nested + inner class DestroyedNavigator { + + @Test + fun `destroyed navigator is destroyed`() { + /* When */ + navigator.onDestroy() + + /* Then */ + expect(navigator.isDestroyed()).toBe(true) + } + + @Test + fun `pushing a scene for destroyed navigator does not notify listeners`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onDestroy() + + /* When */ + navigator.push(scene2) + + /* Then */ + expect(listener.lastScene).toBeNull() + } + + @Test + fun `popping for base and second scene for destroyed navigator does not notify scene`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onDestroy() + navigator.push(scene2) + + /* When */ + navigator.pop() + + /* Then */ + expect(listener.lastScene).toBeNull() + } + + @Test + fun `onBackPressed for only base scene after navigator is destroyed does not notify listeners`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + navigator.onDestroy() + + /* When */ + val result = navigator.onBackPressed() + + /* Then */ + expect(result).toBe(false) + expect(listener.finished).toBe(false) + } + + @Test + fun `finish after navigator is destroyed does not notify listeners`() { + /* Given */ + navigator.addNavigatorEventsListener(listener) + navigator.onStart() + navigator.onDestroy() + + /* When */ + navigator.finish() + + /* Then */ + expect(listener.finished).toBe(false) + } + } + } + + @Nested + inner class SceneInteractionForOnlyBaseScene { + + @Test + fun `starting navigator starts Scene`() { + /* When */ + navigator.onStart() + + /* Then */ + scene1.inOrder { + verify().onStart() + verifyNoMoreInteractions() + } + } + + @Test + fun `starting navigator multiple times starts Scene only once`() { + /* When */ + navigator.onStart() + navigator.onStart() + + /* Then */ + scene1.inOrder { + verify().onStart() + verifyNoMoreInteractions() + } + } + + @Test + fun `stopping an inactive navigator does not stop Scene`() { + /* When */ + navigator.onStop() + + /* Then */ + verifyNoMoreInteractions(scene1) + } + + @Test + fun `stopping an active navigator stops Scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.onStop() + + /* Then */ + scene1.inOrder { + verify().onStart() + verify().onStop() + verifyNoMoreInteractions() + } + } + + @Test + fun `destroy an inactive navigator does not stop Scene`() { + /* When */ + navigator.onDestroy() + + /* Then */ + verify(scene1, never()).onStop() + } + + @Test + fun `destroy an inactive navigator does destroy Scene`() { + /* When */ + navigator.onDestroy() + + /* Then */ + verify(scene1).onDestroy() + } + + @Test + fun `finishing an inactive navigator does not stop Scene`() { + /* When */ + navigator.finish() + + /* Then */ + verify(scene1, never()).onStop() + } + + @Test + fun `finishing an inactive navigator does destroy Scene`() { + /* When */ + navigator.finish() + + /* Then */ + verify(scene1).onDestroy() + } + + @Test + fun `destroy an active navigator stops and destroys Scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.onDestroy() + + /* Then */ + scene1.inOrder { + verify().onStart() + verify().onStop() + verify().onDestroy() + verifyNoMoreInteractions() + } + } + + @Test + fun `finishing an active navigator stops and destroys Scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.finish() + + /* Then */ + scene1.inOrder { + verify().onStart() + verify().onStop() + verify().onDestroy() + verifyNoMoreInteractions() + } + } + + @Test + fun `starting a destroyed navigator does not start Scene`() { + /* Given */ + navigator.onDestroy() + + /* When */ + navigator.onStart() + + /* Then */ + verify(scene1).onDestroy() + verify(scene1, never()).onStart() + } + + @Test + fun `stopping a destroyed navigator does not start Scene`() { + /* Given */ + navigator.onDestroy() + + /* When */ + navigator.onStop() + + /* Then */ + verify(scene1).onDestroy() + verify(scene1, never()).onStop() + } + + @Test + fun `destroying a destroyed navigator only destroys Scene once`() { + /* Given */ + navigator.onDestroy() + + /* When */ + navigator.onDestroy() + + /* Then */ + verify(scene1, times(1)).onDestroy() + } + } + + @Nested + inner class SceneInteractionForBaseAndSecondScenes { + + @BeforeEach + fun before() { + /* Given */ + navigator.push(scene2) + } + + @Test + fun `starting navigator starts both scene`() { + /* When */ + navigator.onStart() + + /* Then */ + inOrder(scene1, scene2) { + verify(scene1).onStart() + verify(scene2).onStart() + verifyNoMoreInteractions() + } + } + + @Test + fun `starting navigator multiple times starts Scene only once`() { + /* When */ + navigator.onStart() + navigator.onStart() + + /* Then */ + inOrder(scene1, scene2) { + verify(scene1).onStart() + verify(scene2).onStart() + verifyNoMoreInteractions() + } + } + + @Test + fun `stopping an inactive navigator does not stop scenes`() { + /* When */ + navigator.onStop() + + /* Then */ + verifyNoMoreInteractions(scene1) + verifyNoMoreInteractions(scene2) + } + + @Test + fun `stopping an active navigator stops both scenes`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.onStop() + + /* Then */ + inOrder(scene1, scene2) { + verify(scene1).onStart() + verify(scene2).onStart() + verify(scene2).onStop() + verify(scene1).onStop() + verifyNoMoreInteractions() + } + } + + @Test + fun `destroy an inactive navigator does not stop Scenes`() { + /* When */ + navigator.onDestroy() + + /* Then */ + verify(scene1, never()).onStop() + verify(scene2, never()).onStop() + } + + @Test + fun `destroy an inactive navigator does destroy Scenes`() { + /* When */ + navigator.onDestroy() + + /* Then */ + inOrder(scene1, scene2) { + verify(scene2).onDestroy() + verify(scene1).onDestroy() + } + } + + @Test + fun `finishing an inactive navigator does not stop Scenes`() { + /* When */ + navigator.finish() + + /* Then */ + verify(scene1, never()).onStop() + verify(scene2, never()).onStop() + } + + @Test + fun `finishing an inactive navigator does destroy Scene`() { + /* When */ + navigator.finish() + + /* Then */ + inOrder(scene1, scene2) { + verify(scene2).onDestroy() + verify(scene1).onDestroy() + } + } + + @Test + fun `destroy an active navigator stops both scenes and destroys both Scenes`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.onDestroy() + + /* Then */ + inOrder(scene1, scene2) { + verify(scene1).onStart() + verify(scene2).onStart() + verify(scene2).onStop() + verify(scene1).onStop() + verify(scene2).onDestroy() + verify(scene1).onDestroy() + verifyNoMoreInteractions() + } + } + + @Test + fun `finishing an active navigator stops both scenes and destroys both Scenes`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.finish() + + /* Then */ + inOrder(scene1, scene2) { + verify(scene1).onStart() + verify(scene2).onStart() + verify(scene2).onStop() + verify(scene1).onStop() + verify(scene2).onDestroy() + verify(scene1).onDestroy() + verifyNoMoreInteractions() + } + } + + @Test + fun `starting a destroyed navigator does not start scenes`() { + /* Given */ + navigator.onDestroy() + + /* When */ + navigator.onStart() + + /* Then */ + verify(scene1).onDestroy() + verify(scene2).onDestroy() + verify(scene1, never()).onStart() + verify(scene2, never()).onStart() + } + + @Test + fun `stopping a destroyed navigator does not stop scenes`() { + /* Given */ + navigator.onDestroy() + + /* When */ + navigator.onStop() + + /* Then */ + verify(scene1).onDestroy() + verify(scene2).onDestroy() + verify(scene1, never()).onStop() + verify(scene2, never()).onStop() + } + + @Test + fun `destroying a destroyed navigator only destroys scenes once`() { + /* Given */ + navigator.onDestroy() + + /* When */ + navigator.onDestroy() + + /* Then */ + verify(scene1, times(1)).onDestroy() + verify(scene2, times(1)).onDestroy() + } + } + + @Nested + inner class SceneInteractionWhenManipulatingStack { + + @Test + fun `popping for only base scene for inactive navigator does not destroy scene`() { + /* When */ + navigator.pop() + + /* When */ + verify(scene1, never()).onDestroy() + } + + @Test + fun `popping for only base scene for inactive navigator does not stop scene`() { + /* When */ + navigator.pop() + + /* When */ + verify(scene1, never()).onStop() + } + + @Test + fun `popping for base and second scene for inactive navigator destroys second scene`() { + /* Given */ + navigator.push(scene2) + + /* When */ + navigator.pop() + + /* When */ + verify(scene2).onDestroy() + } + + @Test + fun `popping for only base scene for active navigator does not stop scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.pop() + + /* When */ + verify(scene1, never()).onStop() + } + + @Test + fun `popping for only base scene for active navigator does not destroy scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.pop() + + /* When */ + verify(scene1, never()).onDestroy() + } + + @Test + fun `popping for base and second scene for active navigator stops and destroys latest scene`() { + /* Given */ + navigator.push(scene2) + navigator.onStart() + + /* When */ + navigator.pop() + + /* When */ + inOrder(scene2) { + verify(scene2).onStop() + verify(scene2).onDestroy() + } + } + + @Test + fun `pushing a new scene for base and second scene for inactive navigator destroys original second scene`() { + /* Given */ + navigator.push(scene2) + + /* When */ + navigator.push(scene3) + + /* When */ + verify(scene2).onDestroy() + } + + @Test + fun `pushing a new scene for base and second scene for inactive navigator does not start newly pushed scene`() { + /* Given */ + navigator.push(scene2) + + /* When */ + navigator.push(scene3) + + /* When */ + verifyNoMoreInteractions(scene3) + } + + @Test + fun `pushing a new scene for base and second scene for active navigator stops and destroys latest scene, and starts newly pushed scene`() { + /* Given */ + navigator.push(scene2) + navigator.onStart() + + /* When */ + navigator.push(scene3) + + /* When */ + inOrder(scene1, scene2, scene3) { + verify(scene2).onStop() + verify(scene2).onDestroy() + verify(scene3).onStart() + } + } + + @Test + fun `onBackPressed for only base scene for inactive navigator destroys scene`() { + /* When */ + navigator.onBackPressed() + + /* When */ + verify(scene1).onDestroy() + } + + @Test + fun `onBackPressed for only base scene for inactive navigator does not stop scene`() { + /* When */ + navigator.onBackPressed() + + /* When */ + verify(scene1, never()).onStop() + } + + @Test + fun `onBackPressed for base and second scene for inactive navigator destroys second scene`() { + /* Given */ + navigator.push(scene2) + + /* When */ + navigator.onBackPressed() + + /* When */ + verify(scene2).onDestroy() + } + + @Test + fun `onBackPressed for only base scene for active navigator stops and destroys scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.onBackPressed() + + /* When */ + scene1.inOrder { + verify().onStop() + verify().onDestroy() + } + } + + @Test + fun `onBackPressed for base and second scene for active navigator stops and destroys second scene, and does not start current scene`() { + /* Given */ + navigator.push(scene2) + navigator.onStart() + + /* When */ + navigator.onBackPressed() + + /* When */ + inOrder(scene1, scene2) { + verify(scene2).onStop() + verify(scene2).onDestroy() + verify(scene1, never()).onStart() + } + } + + @Test + fun `pushing for inactive navigator does not stop base scene`() { + /* When */ + navigator.push(scene2) + + /* Then */ + verify(scene1, never()).onStop() + } + + @Test + fun `pushing for inactive navigator does not start pushed scene`() { + /* When */ + navigator.push(scene2) + + /* Then */ + verify(scene2, never()).onStart() + } + + @Test + fun `pushing for destroyed navigator does not start pushed scene`() { + /* Given */ + navigator.onDestroy() + + /* When */ + navigator.push(scene2) + + /* Then */ + verify(scene2, never()).onStart() + } + + @Test + fun `pushing for started navigator does not stop base scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.push(scene2) + + /* Then */ + inOrder(scene1) { + verify(scene1).onStart() + verify(scene1, never()).onStop() + verifyNoMoreInteractions() + } + } + + @Test + fun `pushing for started navigator starts pushed scene`() { + /* Given */ + navigator.onStart() + + /* When */ + navigator.push(scene2) + + /* Then */ + inOrder(scene1, scene2) { + verify(scene1).onStart() + verify(scene2).onStart() + verifyNoMoreInteractions() + } + } + } + + @Nested + inner class SavingState { + + private val scene1 = SavableTestScene(1) + private val scene2 = SavableTestScene(2) + + private val navigator = + TestConcurrentPairNavigator(scene1) + + @Test + fun `saving and restoring state for only base scene`() { + /* Given */ + navigator.onStart() + scene1.foo = 3 + + /* When */ + val bundle = navigator.saveInstanceState() + scene1.foo = 6 + + val restoredNavigator = + TestConcurrentPairNavigator( + scene1, + bundle + ) + restoredNavigator.onStart() + restoredNavigator.addNavigatorEventsListener(listener) + + /* Then */ + expect(listener.lastScene).toBeInstanceOf { + expect(it.foo).toBe(3) + } + } + + @Test + fun `saving and restoring state for base and second scenes`() { + /* Given */ + navigator.onStart() + scene1.foo = 3 + scene2.foo = 42 + navigator.push(scene2) + + /* When */ + val bundle = navigator.saveInstanceState() + val restoredNavigator = + TestConcurrentPairNavigator( + scene1, + bundle + ) + restoredNavigator.onStart() + restoredNavigator.addNavigatorEventsListener(listener) + + scene1.foo = 1 + scene2.foo = 2 + + /* Then */ + expect(listener.lastScene).toBeInstanceOf { + expect((it.firstScene as SavableTestScene).foo).toBe(3) + expect((it.secondScene as SavableTestScene).foo).toBe(42) + } + } + + @Test + fun `restoring from empty state ignores state`() { + /* When */ + val result = TestConcurrentPairNavigator( + scene1, + NavigatorState() + ) + result.onStart() + result.addNavigatorEventsListener(listener) + + /* Then */ + expect(listener.lastScene).toBe(scene1) + } + + @Test + fun `saved state from callback is the same as saved state _after_ callback`() { + /* Given */ + var state1: SavedState? = null + navigator.addNavigatorEventsListener(object : Navigator.Events { + override fun scene(scene: Scene, data: TransitionData?) { + state1 = navigator.saveInstanceState() + } + + override fun finished() { + } + }) + navigator.onStart() + + /* When */ + navigator.push(scene2) + val state2 = navigator.saveInstanceState() + + /* Then */ + expect(state1).toBe(state2) + } + } + + class TestConcurrentPairNavigator( + private val initialScene: SavableTestScene, + savedState: NavigatorState? = null + ) : ConcurrentPairNavigator(savedState) { + + override fun createInitialScene(): Scene { + return initialScene + } + + override fun instantiateScene(sceneClass: KClass>, state: SceneState?): Scene<*> { + return when (sceneClass) { + SavableTestScene::class -> SavableTestScene.create(state) + else -> error("Unknown class: $sceneClass") + } + } + } + + private open class TestListener : Navigator.Events { + + val scenes = mutableListOf, TransitionData?>>() + val lastScene get() = scenes.lastOrNull()?.first + val lastTransitionData get() = scenes.lastOrNull()?.second + + var finished = false + + override fun scene(scene: Scene, data: TransitionData?) { + scenes += scene to data + } + + override fun finished() { + finished = true + } + } +} diff --git a/ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/SavableTestScene.kt b/ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/SavableTestScene.kt new file mode 100644 index 00000000..3d82b331 --- /dev/null +++ b/ext/acorn/acorn-experimental/src/test/java/com/nhaarman/acorn/navigation/experimental/SavableTestScene.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.navigation.experimental + +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.SavableScene +import com.nhaarman.acorn.presentation.Scene +import com.nhaarman.acorn.state.SceneState +import com.nhaarman.acorn.state.get +import com.nhaarman.acorn.state.sceneState + +open class SavableTestScene(var foo: Int) : Scene, SavableScene { + + var state = State.Created + + override fun onStart() { + state = State.Started + } + + override fun onStop() { + state = State.Stopped + } + + override fun onDestroy() { + state = State.Destroyed + } + + override fun toString(): String { + return "SavableTestScene($foo)" + } + + enum class State { + Created, + Started, + Stopped, + Destroyed + } + + override fun saveInstanceState(): SceneState { + return sceneState { it["foo"] = foo } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SavableTestScene + + if (foo != other.foo) return false + + return true + } + + override fun hashCode(): Int { + return foo + } + + companion object { + + fun create(state: SceneState?): SavableTestScene { + return SavableTestScene( + foo = state?.get("foo") ?: 0 + ) + } + } +} diff --git a/ext/acorn/build.gradle b/ext/acorn/build.gradle index e3b9393d..08fd17e9 100644 --- a/ext/acorn/build.gradle +++ b/ext/acorn/build.gradle @@ -8,7 +8,7 @@ plugins { } dependencies { - api project(':acorn') + api project(":acorn") api "io.arrow-kt:arrow-core" diff --git a/ext/acorn/src/main/java/com/nhaarman/acorn/navigation/StackNavigator.kt b/ext/acorn/src/main/java/com/nhaarman/acorn/navigation/StackNavigator.kt index 3dc14581..dbe74c4c 100644 --- a/ext/acorn/src/main/java/com/nhaarman/acorn/navigation/StackNavigator.kt +++ b/ext/acorn/src/main/java/com/nhaarman/acorn/navigation/StackNavigator.kt @@ -39,8 +39,8 @@ import kotlin.reflect.KClass * This Navigator implements [SavableNavigator] and thus can have its state saved * and restored when necessary. * - * @param savedState An optional instance that contains saved state as returned - * by this class's saveInstanceState() method. + * @param savedState An optional instance that contains the saved state as + * returned by this class's [saveInstanceState] method. */ abstract class StackNavigator( private val savedState: NavigatorState? @@ -108,7 +108,7 @@ abstract class StackNavigator( * Pushes given [scene] onto the stack. * * If this Navigator is currently active, the current [Scene] will be stopped, - * and given [scene] will receive a call to [Navigator.onStart]. + * and given [scene] will receive a call to [Scene.onStart]. * * If this Navigator is currently inactive, no Scene lifecycle events will * be called at all. Starting this Navigator will trigger a call to the @@ -470,4 +470,4 @@ abstract class StackNavigator( } } } -} \ No newline at end of file +} diff --git a/samples/hello-concurrentpairnavigator/build.gradle b/samples/hello-concurrentpairnavigator/build.gradle new file mode 100644 index 00000000..c2c934e5 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/build.gradle @@ -0,0 +1,52 @@ +plugins { + id("com.android.application") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + defaultConfig { + minSdkVersion(23) + } +} + +dependencies { + implementation project(":ext-acorn-experimental") + implementation project(":ext-acorn-rx") + implementation project(":ext-acorn-android-appcompat") + + implementation "org.jetbrains.kotlin:kotlin-stdlib" + + implementation "androidx.core:core-ktx" + + implementation "io.reactivex.rxjava2:rxjava" + implementation "io.reactivex.rxjava2:rxkotlin" + implementation "io.reactivex.rxjava2:rxandroid" + + implementation "com.google.android.material:material" + implementation "androidx.constraintlayout:constraintlayout" + implementation "androidx.appcompat:appcompat" + + testImplementation "com.nhaarman:expect.kt" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin" + testImplementation "org.junit.jupiter:junit-jupiter-api" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" + + debugImplementation project(":ext-acorn-android-testing") + androidTestImplementation "junit:junit" + androidTestImplementation "com.nhaarman:expect.kt" + androidTestImplementation "androidx.test.espresso:espresso-core" + androidTestImplementation "androidx.test.espresso:espresso-contrib" + androidTestImplementation "androidx.test:runner" + androidTestImplementation "androidx.test:rules" +} + +androidExtensions { + experimental = true +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs = ["-Xuse-experimental=kotlin.Experimental"] + } +} diff --git a/samples/hello-concurrentpairnavigator/src/androidTest/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorTest.kt b/samples/hello-concurrentpairnavigator/src/androidTest/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorTest.kt new file mode 100644 index 00000000..2d88dc01 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/androidTest/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.rule.ActivityTestRule +import org.junit.Rule +import org.junit.Test + +class HelloConcurrentPairNavigatorTest { + + @Rule @JvmField val rule = ActivityTestRule(MainActivity::class.java) + + @Test + fun navigatingThroughScenes() { + onView(withText("Hello, first Scene!")).check(matches(isDisplayed())) + onView(withText("Second Scene")).perform(click()) + onView(withText("Hello, first Scene!")).check(matches(isDisplayed())) + onView(withText("Hello, second Scene!")).check(matches(isDisplayed())) + onView(withText("First Scene")).perform(click()) + onView(withText("Hello, first Scene!")).check(matches(isDisplayed())) + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/AndroidManifest.xml b/samples/hello-concurrentpairnavigator/src/main/AndroidManifest.xml new file mode 100644 index 00000000..933ddb32 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstScene.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstScene.kt new file mode 100644 index 00000000..f8160e69 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstScene.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import android.view.View +import android.view.ViewGroup +import com.nhaarman.acorn.android.presentation.ProvidesView +import com.nhaarman.acorn.android.presentation.RestorableViewController +import com.nhaarman.acorn.android.presentation.ViewController +import com.nhaarman.acorn.android.util.inflate +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.RxScene +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxkotlin.plusAssign +import kotlinx.android.synthetic.main.first_scene.* +import java.util.concurrent.TimeUnit + +class FirstScene( + private val listener: Events, + scheduler: Scheduler = AndroidSchedulers.mainThread() +) : RxScene(null), ProvidesView { + + private val counter = Observable + .interval(0, 100, TimeUnit.MILLISECONDS, scheduler) + .replay(1).autoConnect(this) + + override fun createViewController(parent: ViewGroup): ViewController { + return FirstSceneViewController(parent.inflate(R.layout.first_scene)) + } + + override fun onStart() { + super.onStart() + + disposables += counter + .combineWithLatestView() + .subscribe { (count, container) -> + container?.count = count + } + } + + override fun attach(v: FirstSceneContainer) { + super.attach(v) + v.onActionClicked { listener.actionClicked() } + } + + interface Events { + + fun actionClicked() + } +} + +interface FirstSceneContainer : Container { + + var count: Long + + fun onActionClicked(f: () -> Unit) +} + +class FirstSceneViewController( + override val view: View +) : RestorableViewController, FirstSceneContainer { + + override var count: Long = 0 + set(value) { + counterTV.text = value.toString() + } + + override fun onActionClicked(f: () -> Unit) { + secondSceneButton.setOnClickListener { f() } + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondTransition.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondTransition.kt new file mode 100644 index 00000000..6f0f768b --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondTransition.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import android.view.ViewGroup +import androidx.core.view.doOnPreDraw +import com.nhaarman.acorn.android.transition.Transition +import com.nhaarman.acorn.android.util.inflateView +import com.nhaarman.acorn.navigation.experimental.ExperimentalConcurrentPairNavigator +import kotlinx.android.synthetic.main.second_scene.view.* + +@UseExperimental(ExperimentalConcurrentPairNavigator::class) +object FirstSecondTransition : Transition { + + override fun execute(parent: ViewGroup, callback: Transition.Callback) { + val secondScene = parent.inflateView(R.layout.second_scene) + parent.addView(secondScene) + + val viewController = FirstSecondViewController(parent) + callback.attach(viewController) + + parent.doOnPreDraw { + secondScene.overlayView + .apply { + alpha = 0f + animate().alpha(1f) + } + + secondScene.cardView.apply { + translationY = height.toFloat() + animate().translationY(0f) + .withEndAction { + callback.onComplete(viewController) + } + } + } + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondViewController.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondViewController.kt new file mode 100644 index 00000000..a1d6535a --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSecondViewController.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import android.view.ViewGroup +import com.nhaarman.acorn.android.presentation.ViewController +import com.nhaarman.acorn.navigation.experimental.CombinedContainer +import com.nhaarman.acorn.navigation.experimental.ExperimentalConcurrentPairNavigator +import com.nhaarman.acorn.presentation.Container +import kotlinx.android.synthetic.main.first_scene.view.* +import kotlinx.android.synthetic.main.second_scene.view.* + +@UseExperimental(ExperimentalConcurrentPairNavigator::class) +class FirstSecondViewController( + override val view: ViewGroup +) : ViewController, CombinedContainer { + + override val firstContainer: Container by lazy { + FirstSceneViewController(view.firstSceneRoot) + } + + override val secondContainer: Container by lazy { + SecondSceneViewController(view.secondSceneRoot) + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigator.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigator.kt new file mode 100644 index 00000000..53b0153a --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigator.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import com.nhaarman.acorn.navigation.experimental.ConcurrentPairNavigator +import com.nhaarman.acorn.navigation.experimental.ExperimentalConcurrentPairNavigator +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.Scene +import com.nhaarman.acorn.state.SceneState +import kotlin.reflect.KClass + +@UseExperimental(ExperimentalConcurrentPairNavigator::class) +class HelloConcurrentPairNavigator : ConcurrentPairNavigator(null) { + + override fun createInitialScene(): Scene { + return FirstScene(FirstSceneListener()) + } + + override fun instantiateScene(sceneClass: KClass>, state: SceneState?): Scene { + return when (sceneClass) { + FirstScene::class -> FirstScene(FirstSceneListener()) + SecondScene::class -> SecondScene(SecondSceneListener()) + else -> error("Unknown scene: $sceneClass") + } + } + + inner class FirstSceneListener : FirstScene.Events { + + override fun actionClicked() { + push(SecondScene(SecondSceneListener())) + } + } + + inner class SecondSceneListener : SecondScene.Events { + + override fun onBackClicked() { + pop() + } + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorProvider.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorProvider.kt new file mode 100644 index 00000000..1a1eb18f --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import com.nhaarman.acorn.state.NavigatorState +import com.nhaarman.acorn.android.navigation.AbstractNavigatorProvider + +object HelloConcurrentPairNavigatorProvider : AbstractNavigatorProvider() { + + override fun createNavigator(savedState: NavigatorState?): HelloConcurrentPairNavigator { + return HelloConcurrentPairNavigator() + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorViewControllerFactory.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorViewControllerFactory.kt new file mode 100644 index 00000000..dc7c4c3a --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/HelloConcurrentPairNavigatorViewControllerFactory.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import android.view.ViewGroup +import com.nhaarman.acorn.android.presentation.ViewController +import com.nhaarman.acorn.android.presentation.ViewControllerFactory +import com.nhaarman.acorn.android.util.inflate +import com.nhaarman.acorn.presentation.Scene +import com.nhaarman.acorn.presentation.SceneKey + +class HelloConcurrentPairNavigatorViewControllerFactory : ViewControllerFactory { + + override fun supports(scene: Scene<*>): Boolean { + return scene.key == SceneKey.defaultKey() + } + + override fun viewControllerFor(scene: Scene<*>, parent: ViewGroup): ViewController { + return FirstSecondViewController(parent.inflate(R.layout.first_and_second_scene)) + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MainActivity.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MainActivity.kt new file mode 100644 index 00000000..3399c7e6 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MainActivity.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import com.nhaarman.acorn.android.AcornAppCompatActivity +import com.nhaarman.acorn.android.navigation.NavigatorProvider +import com.nhaarman.acorn.android.presentation.ViewControllerFactory +import com.nhaarman.acorn.android.transition.TransitionFactory +import com.nhaarman.acorn.android.transition.transitionFactory +import com.nhaarman.acorn.presentation.SceneKey + +class MainActivity : AcornAppCompatActivity() { + + override fun provideNavigatorProvider(): NavigatorProvider { + return HelloConcurrentPairNavigatorProvider + } + + override fun provideViewControllerFactory(): ViewControllerFactory { + return HelloConcurrentPairNavigatorViewControllerFactory() + } + + override fun provideTransitionFactory(viewControllerFactory: ViewControllerFactory): TransitionFactory { + return transitionFactory(viewControllerFactory) { + (SceneKey.defaultKey() to SceneKey.defaultKey()) use FirstSecondTransition + (SceneKey.defaultKey() to SceneKey.defaultKey()) use SecondFirstTransition + } + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MyApplication.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MyApplication.kt new file mode 100644 index 00000000..55b197ff --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/MyApplication.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import android.app.Application +import com.nhaarman.acorn.android.AndroidLogger + +class MyApplication : Application() { + + override fun onCreate() { + acorn.logger = AndroidLogger() + super.onCreate() + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondFirstTransition.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondFirstTransition.kt new file mode 100644 index 00000000..159a4dc7 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondFirstTransition.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import android.view.ViewGroup +import com.nhaarman.acorn.android.transition.Transition +import kotlinx.android.synthetic.main.first_and_second_scene.view.* +import kotlinx.android.synthetic.main.first_scene.view.* +import kotlinx.android.synthetic.main.second_scene.view.* + +object SecondFirstTransition : Transition { + + override fun execute(parent: ViewGroup, callback: Transition.Callback) { + val firstAndSecondRoot = parent.findViewById(R.id.firstAndSecondRoot) + + if (firstAndSecondRoot != null) { + normalizeLayout(parent) + } + + val viewController = FirstSceneViewController(parent) + callback.attach(viewController) + + val overlayView = parent.secondSceneRoot.overlayView + val cardView = parent.secondSceneRoot.cardView + + overlayView.animate() + .alpha(0f) + + cardView.animate() + .translationY(cardView.height.toFloat()) + .withEndAction { + parent.removeView(parent.secondSceneRoot) + callback.onComplete(viewController) + } + } + + /** + * 'Normalizes' the layout by removing the intermediate FrameLayout. + * The resulting layout in [parent] contains the exact layout as it would + * be when transitioning using [FirstSecondTransition]. + */ + private fun normalizeLayout(parent: ViewGroup) { + val firstAndSecondRoot = parent.firstAndSecondRoot + + firstAndSecondRoot.firstSceneRoot.let { + firstAndSecondRoot.removeView(it) + parent.addView(it) + } + + firstAndSecondRoot.secondSceneRoot.let { + firstAndSecondRoot.removeView(it) + parent.addView(it) + } + + parent.removeView(firstAndSecondRoot) + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondScene.kt b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondScene.kt new file mode 100644 index 00000000..2df5346a --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/SecondScene.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Niek Haarman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nhaarman.acorn.samples.helloconcurrentpairnavigator + +import android.view.View +import com.nhaarman.acorn.android.presentation.RestorableViewController +import com.nhaarman.acorn.presentation.Container +import com.nhaarman.acorn.presentation.Scene +import kotlinx.android.synthetic.main.second_scene.* + +class SecondScene( + private val listener: Events +) : Scene { + + override fun attach(v: SecondSceneContainer) { + v.onBackClicked { listener.onBackClicked() } + } + + interface Events { + + fun onBackClicked() + } +} + +interface SecondSceneContainer : Container { + + fun onBackClicked(f: () -> Unit) +} + +class SecondSceneViewController( + override val view: View +) : RestorableViewController, SecondSceneContainer { + + override fun onBackClicked(f: () -> Unit) { + firstSceneButton.setOnClickListener { f() } + } +} diff --git a/samples/hello-concurrentpairnavigator/src/main/res/layout/first_and_second_scene.xml b/samples/hello-concurrentpairnavigator/src/main/res/layout/first_and_second_scene.xml new file mode 100644 index 00000000..fb8f32e2 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/res/layout/first_and_second_scene.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/samples/hello-concurrentpairnavigator/src/main/res/layout/first_scene.xml b/samples/hello-concurrentpairnavigator/src/main/res/layout/first_scene.xml new file mode 100644 index 00000000..edcd7075 --- /dev/null +++ b/samples/hello-concurrentpairnavigator/src/main/res/layout/first_scene.xml @@ -0,0 +1,36 @@ + + + + + + + +