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