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 be624a3c..97ef411e 100644
--- a/.idea/inspectionProfiles/ktlint.xml
+++ b/.idea/inspectionProfiles/ktlint.xml
@@ -7,5 +7,7 @@
+
+
\ 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/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 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/hello-concurrentpairnavigator/src/main/res/layout/second_scene.xml b/samples/hello-concurrentpairnavigator/src/main/res/layout/second_scene.xml
new file mode 100644
index 00000000..3aad2a3f
--- /dev/null
+++ b/samples/hello-concurrentpairnavigator/src/main/res/layout/second_scene.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/hello-concurrentpairnavigator/src/main/res/values/strings.xml b/samples/hello-concurrentpairnavigator/src/main/res/values/strings.xml
new file mode 100644
index 00000000..d46ec76d
--- /dev/null
+++ b/samples/hello-concurrentpairnavigator/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+
+ Hello, first Scene!
+ Second Scene
+
+ Hello, second Scene!
+ First Scene
+
+
\ No newline at end of file
diff --git a/samples/hello-concurrentpairnavigator/src/main/res/values/styles.xml b/samples/hello-concurrentpairnavigator/src/main/res/values/styles.xml
new file mode 100644
index 00000000..eaeb02dd
--- /dev/null
+++ b/samples/hello-concurrentpairnavigator/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/hello-concurrentpairnavigator/src/test/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSceneTest.kt b/samples/hello-concurrentpairnavigator/src/test/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSceneTest.kt
new file mode 100644
index 00000000..dea33a3c
--- /dev/null
+++ b/samples/hello-concurrentpairnavigator/src/test/java/com/nhaarman/acorn/samples/helloconcurrentpairnavigator/FirstSceneTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.expect.expect
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.verify
+import io.reactivex.schedulers.TestScheduler
+import org.junit.jupiter.api.Test
+import java.util.concurrent.TimeUnit
+
+class FirstSceneTest {
+
+ private val scheduler = TestScheduler()
+ private val listener = mock()
+ private val scene = FirstScene(listener, scheduler)
+ private val container = TestContainer()
+
+ @Test
+ fun `clicking button notifies listener`() {
+ /* Given */
+ scene.attach(container)
+
+ /* When */
+ container.clickAction()
+
+ /* Then */
+ verify(listener).actionClicked()
+ }
+
+ @Test
+ fun `count is applied to the container when attached later`() {
+ /* Given */
+ scene.onStart()
+
+ /* When */
+ scheduler.advanceTimeBy(5, TimeUnit.SECONDS)
+ scene.attach(container)
+
+ /* Then */
+ expect(container.count).toBe(50)
+ }
+
+ @Test
+ fun `count is applied to the container after attaching`() {
+ /* Given */
+ scene.onStart()
+
+ /* When */
+ scene.attach(container)
+ scheduler.advanceTimeBy(5, TimeUnit.SECONDS)
+
+ /* Then */
+ expect(container.count).toBe(50)
+ }
+
+ private class TestContainer : FirstSceneContainer {
+
+ override var count: Long = 0
+
+ fun clickAction() {
+ listener?.invoke()
+ }
+
+ private var listener: (() -> Unit)? = null
+ override fun onActionClicked(f: () -> Unit) {
+ listener = f
+ }
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index fe51081f..66fdb2f7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,6 +6,7 @@ include(":ext")
include(":ext-acorn")
include(":ext-acorn-testing")
include(":ext-acorn-rx")
+include(":ext-acorn-experimental")
include(":ext-acorn-android")
include(":ext-acorn-android-testing")
@@ -13,6 +14,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")
@@ -21,6 +25,7 @@ include(":samples:hello-viewfactory")
include(":samples:hello-startactivity")
include(":samples:hello-sharedata")
include(":samples:hello-transitionanimation")
+include(':samples:hello-concurrentpairnavigator')
include(":samples:notes-app")
include(":samples:notes-app:android")
@@ -31,6 +36,7 @@ project(":acorn-android").projectDir = file("core/acorn-android-core")
project(":ext-acorn").projectDir = file("ext/acorn")
project(":ext-acorn-testing").projectDir = file("ext/acorn/acorn-testing")
project(":ext-acorn-rx").projectDir = file("ext/acorn/acorn-rx")
+project(":ext-acorn-experimental").projectDir = file("ext/acorn/acorn-experimental")
project(":ext-acorn-android").projectDir = file("ext/acorn-android")
project(":ext-acorn-android-testing").projectDir = file("ext/acorn-android/acorn-android-testing")
@@ -38,5 +44,6 @@ 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")
-enableFeaturePreview("STABLE_PUBLISHING")
\ No newline at end of file
+enableFeaturePreview("STABLE_PUBLISHING")
diff --git a/test b/test
index 56aa3695..7269102f 100755
--- a/test
+++ b/test
@@ -9,13 +9,14 @@ adb devices | tail -n +2 | cut -sf 1 | xargs -I {} adb -s {} shell settings put
:ext-acorn-android-testing:lint \
: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-startactivity:lintRelease \
+ :samples:hello-staterestoration:lintRelease \
:samples:hello-transitionanimation:lintRelease \
:samples:hello-viewfactory:lintRelease \
+ :samples:hello-world:lintRelease \
:samples:notes-app:android:lintRelease \
packageDebugAndroidTest \
--rerun-tasks \