Made NiaNavigator a stateless class only responsibly for navigating and pop (modifying backStack). Navigation state now lives in a new class called NiaNavigatorState. The state of this class is saved and restored by ViewModel.pull/1902/head
parent
a878b170dc
commit
b91a965ae2
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.google.samples.apps.nowinandroid.core.navigation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import kotlin.collections.mutableListOf
|
||||
|
||||
// TODO refine back behavior - perhaps take a lambda so that each screen / use site can customize back behavior?
|
||||
// https://github.com/android/nowinandroid/issues/1934
|
||||
class NiaBackStack(
|
||||
private val startKey: NiaNavKey,
|
||||
) {
|
||||
internal var backStackMap: LinkedHashMap<NiaNavKey, MutableList<NiaNavKey>> =
|
||||
linkedMapOf(
|
||||
startKey to mutableListOf(startKey),
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
val backStack: SnapshotStateList<NiaNavKey> = mutableStateListOf(startKey)
|
||||
|
||||
var currentTopLevelKey: NiaNavKey by mutableStateOf(backStackMap.keys.last())
|
||||
private set
|
||||
|
||||
@get:VisibleForTesting
|
||||
val currentKey: NiaNavKey
|
||||
get() = backStackMap[currentTopLevelKey]!!.last()
|
||||
|
||||
fun navigate(key: NiaNavKey) {
|
||||
when {
|
||||
// top level singleTop -> clear substack
|
||||
key == currentTopLevelKey -> backStackMap[key] = mutableListOf(key)
|
||||
// top level non-singleTop
|
||||
key.isTopLevel -> {
|
||||
// if navigating back to start destination, pop all other top destinations and
|
||||
// store start destination substack
|
||||
if (key == startKey) {
|
||||
val tempStack = mapOf(startKey to backStackMap[startKey]!!)
|
||||
backStackMap.clear()
|
||||
backStackMap.putAll(tempStack)
|
||||
// else either restore an existing substack or initiate new one
|
||||
} else {
|
||||
backStackMap[key] = backStackMap.remove(key) ?: mutableListOf(key)
|
||||
}
|
||||
}
|
||||
// not top level - add to current substack
|
||||
else -> {
|
||||
val currentStack = backStackMap.values.last()
|
||||
// single top
|
||||
if (currentStack.lastOrNull() == key) {
|
||||
currentStack.removeLastOrNull()
|
||||
}
|
||||
currentStack.add(key)
|
||||
}
|
||||
}
|
||||
updateBackStack()
|
||||
}
|
||||
|
||||
fun popLast(count: Int = 1) {
|
||||
var popCount = count
|
||||
var currentEntry = backStackMap.entries.last()
|
||||
while (popCount > 0) {
|
||||
val currentStack = currentEntry.value
|
||||
if (currentStack.size == 1) {
|
||||
// if current sub-stack only has one key, remove the sub-stack from the map
|
||||
backStackMap.remove(currentEntry.key)
|
||||
when {
|
||||
// throw if map is empty after pop
|
||||
backStackMap.isEmpty() -> error(popErrorMessage(count, currentEntry.key))
|
||||
// otherwise update currentEntry
|
||||
else -> currentEntry = backStackMap.entries.last()
|
||||
}
|
||||
} else {
|
||||
// if current sub-stack has more than one key, just pop the last key off the sub-stack
|
||||
currentStack.removeLastOrNull()
|
||||
}
|
||||
popCount--
|
||||
}
|
||||
updateBackStack()
|
||||
}
|
||||
|
||||
private fun updateBackStack() {
|
||||
backStack.apply {
|
||||
clear()
|
||||
backStack.addAll(
|
||||
backStackMap.flatMap { it.value },
|
||||
)
|
||||
}
|
||||
|
||||
currentTopLevelKey = backStackMap.keys.last()
|
||||
}
|
||||
|
||||
internal fun restore(map: LinkedHashMap<NiaNavKey, MutableList<NiaNavKey>>?) {
|
||||
map ?: return
|
||||
backStackMap.clear()
|
||||
backStackMap.putAll(map)
|
||||
updateBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
interface NiaNavKey {
|
||||
val isTopLevel: Boolean
|
||||
}
|
||||
|
||||
private fun popErrorMessage(count: Int, lastPopped: NiaNavKey) =
|
||||
"""
|
||||
Failed to pop $count entries. BackStack has been popped to an empty stack. Last
|
||||
popped key is $lastPopped.
|
||||
""".trimIndent()
|
||||
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.google.samples.apps.nowinandroid.core.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.plus
|
||||
|
||||
class NiaNavigatorState(
|
||||
internal val startKey: NiaNavKey,
|
||||
) {
|
||||
internal var backStacks: MutableMap<NiaNavKey, SnapshotStateList<NiaNavKey>> =
|
||||
linkedMapOf(
|
||||
startKey to mutableStateListOf(startKey),
|
||||
)
|
||||
|
||||
val activeTopLeveLKeys: SnapshotStateList<NiaNavKey> = mutableStateListOf(startKey)
|
||||
|
||||
var currentTopLevelKey: NiaNavKey by mutableStateOf(activeTopLeveLKeys.last())
|
||||
private set
|
||||
|
||||
@get:VisibleForTesting
|
||||
val currentBackStack: List<NiaNavKey>
|
||||
get() = activeTopLeveLKeys.fold(mutableListOf()) { list, topLevelKey ->
|
||||
list.apply {
|
||||
addAll(backStacks[topLevelKey]!!)
|
||||
}
|
||||
}
|
||||
|
||||
@get:VisibleForTesting
|
||||
val currentKey: NiaNavKey
|
||||
get() = backStacks[currentTopLevelKey]!!.last()
|
||||
|
||||
internal fun updateActiveTopLevelKeys(activeKeys: List<NiaNavKey>) {
|
||||
check(activeKeys.isNotEmpty()) { "List of active top-level keys should not be empty" }
|
||||
activeTopLeveLKeys.clear()
|
||||
activeTopLeveLKeys.addAll(activeKeys)
|
||||
currentTopLevelKey = activeTopLeveLKeys.last()
|
||||
}
|
||||
|
||||
internal fun restore(activeKeys: List<NiaNavKey>, map: LinkedHashMap<NiaNavKey, SnapshotStateList<NiaNavKey>>?) {
|
||||
map ?: return
|
||||
backStacks.clear()
|
||||
map.forEach { entry ->
|
||||
backStacks[entry.key] = entry.value.toMutableStateList()
|
||||
}
|
||||
updateActiveTopLevelKeys(activeKeys)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/android/nowinandroid/issues/1934
|
||||
class NiaNavigator @Inject constructor(
|
||||
val navigatorState: NiaNavigatorState
|
||||
) {
|
||||
fun navigate(key: NiaNavKey) {
|
||||
val currentActiveSubStacks = linkedSetOf<NiaNavKey>()
|
||||
navigatorState.apply {
|
||||
currentActiveSubStacks.addAll(activeTopLeveLKeys)
|
||||
when {
|
||||
// top level singleTop -> clear substack
|
||||
key == currentTopLevelKey -> {
|
||||
backStacks[key] = mutableStateListOf(key)
|
||||
// no change to currentActiveTabs
|
||||
}
|
||||
// top level non-singleTop
|
||||
key.isTopLevel -> {
|
||||
// if navigating back to start destination, then only show the starting substack
|
||||
if (key == startKey) {
|
||||
currentActiveSubStacks.clear()
|
||||
currentActiveSubStacks.add(key)
|
||||
} else {
|
||||
// else either restore an existing substack or initiate new one
|
||||
backStacks[key] = backStacks.remove(key) ?: mutableStateListOf(key)
|
||||
// move this top level key to the top of active substacks
|
||||
currentActiveSubStacks.remove(key)
|
||||
currentActiveSubStacks.add(key)
|
||||
}
|
||||
}
|
||||
// not top level - add to current substack
|
||||
else -> {
|
||||
val currentStack = backStacks[currentTopLevelKey]!!
|
||||
// single top
|
||||
if (currentStack.lastOrNull() == key) {
|
||||
currentStack.removeLastOrNull()
|
||||
}
|
||||
currentStack.add(key)
|
||||
// no change to currentActiveTabs
|
||||
}
|
||||
}
|
||||
updateActiveTopLevelKeys(currentActiveSubStacks.toList())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun pop() {
|
||||
navigatorState.apply {
|
||||
val currentSubstack = backStacks[currentTopLevelKey]!!
|
||||
if (currentSubstack.size == 1) {
|
||||
// if current sub-stack only has one key, remove the sub-stack from the map
|
||||
currentSubstack.removeLastOrNull()
|
||||
backStacks.remove(currentTopLevelKey)
|
||||
updateActiveTopLevelKeys(activeTopLeveLKeys.dropLast(1))
|
||||
} else {
|
||||
currentSubstack.removeLastOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface NiaNavKey {
|
||||
val isTopLevel: Boolean
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun NiaNavigatorState.getEntries(
|
||||
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
|
||||
): List<NavEntry<NiaNavKey>> =
|
||||
activeTopLeveLKeys.fold(emptyList()) { entries, topLevelKey ->
|
||||
val decorated = key(topLevelKey) {
|
||||
val decorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator<NiaNavKey>()
|
||||
)
|
||||
rememberDecoratedNavEntries(
|
||||
backStack = backStacks[topLevelKey]!!,
|
||||
entryDecorators = decorators,
|
||||
entryProvider = entryProvider {
|
||||
entryProviderBuilders.forEach { builder ->
|
||||
builder()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
entries + decorated
|
||||
}
|
||||
@ -1,280 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.google.samples.apps.nowinandroid.core.navigation
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NiaBackStackTest {
|
||||
|
||||
private lateinit var niaBackStack: NiaBackStack
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
niaBackStack = NiaBackStack(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStartKey() {
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigate() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevel() {
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateSingleTop() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevelSingleTop() {
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubStack() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiStack() {
|
||||
// add to start stack
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
|
||||
// navigate to new top level
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// add to new stack
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// go back to start stack
|
||||
niaBackStack.navigate(TestStartKey)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestore() {
|
||||
assertThat(niaBackStack.backStack).containsExactly(TestStartKey)
|
||||
|
||||
niaBackStack.restore(
|
||||
linkedMapOf(
|
||||
TestStartKey to mutableListOf(TestStartKey, TestKeyFirst),
|
||||
TestTopLevelKey to mutableListOf(TestTopLevelKey, TestKeySecond),
|
||||
),
|
||||
)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKey,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneNonTopLevel() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.popLast()
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneTopLevel() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// remove TopLevel
|
||||
niaBackStack.popLast()
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleNonTopLevel() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.popLast(2)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleTopLevel() {
|
||||
val testTopLevelKeyTwo = object : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
// second sub-stack
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
// third sub-stack
|
||||
niaBackStack.navigate(testTopLevelKeyTwo)
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
TestKeyFirst,
|
||||
testTopLevelKeyTwo,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.popLast(4)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun throwOnEmptyBackStack() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
niaBackStack.popLast(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object TestStartKey : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
private object TestTopLevelKey : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
private object TestKeyFirst : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
private object TestKeySecond : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.google.samples.apps.nowinandroid.core.navigation
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NiaNavigatorStateTest {
|
||||
|
||||
private lateinit var niaNavigatorState: NiaNavigatorState
|
||||
private lateinit var niaNavigator: NiaNavigator
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
niaNavigatorState = NiaNavigatorState(TestStartKey)
|
||||
niaNavigator = NiaNavigator(niaNavigatorState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStartKey() {
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigate() {
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevel() {
|
||||
niaNavigator.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateSingleTop() {
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevelSingleTop() {
|
||||
niaNavigator.navigate(TestTopLevelKey)
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
niaNavigator.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubStack() {
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
|
||||
niaNavigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiStack() {
|
||||
// add to start stack
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
|
||||
// navigate to new top level
|
||||
niaNavigator.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// add to new stack
|
||||
niaNavigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// go back to start stack
|
||||
niaNavigator.navigate(TestStartKey)
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestore() {
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(TestStartKey)
|
||||
|
||||
niaNavigatorState.restore(
|
||||
listOf(TestStartKey, TestTopLevelKey),
|
||||
linkedMapOf(
|
||||
TestStartKey to mutableStateListOf(TestStartKey, TestKeyFirst),
|
||||
TestTopLevelKey to mutableStateListOf(TestTopLevelKey, TestKeySecond),
|
||||
),
|
||||
)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKey,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneNonTopLevel() {
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
niaNavigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaNavigator.pop()
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneTopLevel() {
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
niaNavigator.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// remove TopLevel
|
||||
niaNavigator.pop()
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleNonTopLevel() {
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
niaNavigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaNavigator.pop()
|
||||
niaNavigator.pop()
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleTopLevel() {
|
||||
val testTopLevelKeyTwo = object : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
// second sub-stack
|
||||
niaNavigator.navigate(TestTopLevelKey)
|
||||
niaNavigator.navigate(TestKeyFirst)
|
||||
// third sub-stack
|
||||
niaNavigator.navigate(testTopLevelKeyTwo)
|
||||
niaNavigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
TestKeyFirst,
|
||||
testTopLevelKeyTwo,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
repeat(4) {
|
||||
niaNavigator.pop()
|
||||
}
|
||||
|
||||
assertThat(niaNavigatorState.currentBackStack).containsExactly(
|
||||
TestStartKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun throwOnEmptyBackStack() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
niaNavigator.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object TestStartKey : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
private object TestTopLevelKey : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
private object TestKeyFirst : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
private object TestKeySecond : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
Loading…
Reference in new issue