parent
386d1a0a16
commit
f6824858c4
@ -1,62 +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.di
|
|
||||||
|
|
||||||
// TODO: Rename to `NiaNavigationStateProvider`
|
|
||||||
// Does this even need to be injected? Can't we just instantiate it directly using `rememberNavigationState`?
|
|
||||||
/*
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object NavigationStateProvider {
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideNavigationState(): NiaNavigationState =
|
|
||||||
NiaNavigationState(
|
|
||||||
//startKey = TopLevelDestination.FOR_YOU.key,
|
|
||||||
startKey = object : NiaNavKey {
|
|
||||||
override val isTopLevel: Boolean
|
|
||||||
get() = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Remove commented out code
|
|
||||||
//
|
|
||||||
// @Provides
|
|
||||||
// @Singleton
|
|
||||||
// fun provideNiaNavigator(
|
|
||||||
// state: NiaNavigatorState
|
|
||||||
// ): NiaNavigator =
|
|
||||||
// NiaNavigator(state)
|
|
||||||
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Registers feature modules' polymorphic serializers to support
|
|
||||||
* feature keys' save and restore by savedstate
|
|
||||||
* in [com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackViewModel].
|
|
||||||
*//*
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideSerializersModule(
|
|
||||||
polymorphicModuleBuilders: Set<@JvmSuppressWildcards PolymorphicModuleBuilder<NiaNavKey>.() -> Unit>,
|
|
||||||
): SerializersModule = SerializersModule {
|
|
||||||
polymorphic(NiaNavKey::class) {
|
|
||||||
polymorphicModuleBuilders.forEach { it() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 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.navigation
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
|
||||||
import com.google.samples.apps.nowinandroid.R
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for the top level destinations in the application. Contains metadata about the destination
|
|
||||||
* that is used in the top app bar and common navigation UI.
|
|
||||||
*
|
|
||||||
* @param selectedIcon The icon to be displayed in the navigation UI when this destination is
|
|
||||||
* selected.
|
|
||||||
* @param unselectedIcon The icon to be displayed in the navigation UI when this destination is
|
|
||||||
* not selected.
|
|
||||||
* @param iconTextId Text that to be displayed in the navigation UI.
|
|
||||||
* @param titleTextId Text that is displayed on the top app bar.
|
|
||||||
* @param route The route to use when navigating to this destination.
|
|
||||||
* @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that
|
|
||||||
* there is a single destination in that section of the app (no nested destinations).
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
enum class TopLevelDestination(
|
|
||||||
val selectedIcon: ImageVector,
|
|
||||||
val unselectedIcon: ImageVector,
|
|
||||||
@StringRes val iconTextId: Int,
|
|
||||||
@StringRes val titleTextId: Int,
|
|
||||||
val route: KClass<*>,
|
|
||||||
val key: NiaNavKey,
|
|
||||||
) {
|
|
||||||
FOR_YOU(
|
|
||||||
selectedIcon = NiaIcons.Upcoming,
|
|
||||||
unselectedIcon = NiaIcons.UpcomingBorder,
|
|
||||||
iconTextId = forYouR.string.feature_foryou_api_title,
|
|
||||||
titleTextId = R.string.app_name,
|
|
||||||
route = ForYouRoute::class,
|
|
||||||
key = ForYouRoute,
|
|
||||||
),
|
|
||||||
BOOKMARKS(
|
|
||||||
selectedIcon = NiaIcons.Bookmarks,
|
|
||||||
unselectedIcon = NiaIcons.BookmarksBorder,
|
|
||||||
iconTextId = bookmarksR.string.feature_bookmarks_api_title,
|
|
||||||
titleTextId = bookmarksR.string.feature_bookmarks_api_title,
|
|
||||||
route = BookmarksRoute::class,
|
|
||||||
key = BookmarksRoute,
|
|
||||||
),
|
|
||||||
INTERESTS(
|
|
||||||
selectedIcon = NiaIcons.Grid3x3,
|
|
||||||
unselectedIcon = NiaIcons.Grid3x3,
|
|
||||||
iconTextId = searchR.string.feature_search_api_interests,
|
|
||||||
titleTextId = searchR.string.feature_search_api_interests,
|
|
||||||
route = InterestsRoute::class,
|
|
||||||
key = InterestsRoute(null),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val TopLevelDestinations = TopLevelDestination.entries.associateBy { dest -> dest.key }
|
|
||||||
*/
|
|
||||||
|
|
||||||
val FOR_YOU = NavBarItem(
|
|
||||||
selectedIcon = NiaIcons.Upcoming,
|
|
||||||
unselectedIcon = NiaIcons.UpcomingBorder,
|
|
||||||
iconTextId = forYouR.string.feature_foryou_api_title,
|
|
||||||
titleTextId = R.string.app_name,
|
|
||||||
key = ForYouRoute,
|
|
||||||
)
|
|
||||||
|
|
||||||
val BOOKMARKS = NavBarItem(
|
|
||||||
selectedIcon = NiaIcons.Bookmarks,
|
|
||||||
unselectedIcon = NiaIcons.BookmarksBorder,
|
|
||||||
iconTextId = bookmarksR.string.feature_bookmarks_api_title,
|
|
||||||
titleTextId = bookmarksR.string.feature_bookmarks_api_title,
|
|
||||||
key = BookmarksRoute,
|
|
||||||
)
|
|
||||||
|
|
||||||
val INTERESTS = NavBarItem(
|
|
||||||
selectedIcon = NiaIcons.Grid3x3,
|
|
||||||
unselectedIcon = NiaIcons.Grid3x3,
|
|
||||||
iconTextId = searchR.string.feature_search_api_interests,
|
|
||||||
titleTextId = searchR.string.feature_search_api_interests,
|
|
||||||
key = InterestsRoute(null)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
val TOP_LEVEL_ROUTES = mapOf<NavKey, NavBarItem>(
|
|
||||||
ForYouRoute to FOR_YOU,
|
|
||||||
BookmarksRoute to BOOKMARKS,
|
|
||||||
InterestsRoute(null) to INTERESTS,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for the top level navigation items in the application. Contains UI information about the
|
|
||||||
* current route that is used in the top app bar and common navigation UI.
|
|
||||||
*
|
|
||||||
* @param selectedIcon The icon to be displayed in the navigation UI when this destination is
|
|
||||||
* selected.
|
|
||||||
* @param unselectedIcon The icon to be displayed in the navigation UI when this destination is
|
|
||||||
* not selected.
|
|
||||||
* @param iconTextId Text that to be displayed in the navigation UI.
|
|
||||||
* @param titleTextId Text that is displayed on the top app bar.
|
|
||||||
* @param key The navigation key to use when navigating to this destination.
|
|
||||||
* there is a single destination in that section of the app (no nested destinations).
|
|
||||||
*/
|
|
||||||
data class NavBarItem(
|
|
||||||
val selectedIcon: ImageVector,
|
|
||||||
val unselectedIcon: ImageVector,
|
|
||||||
@StringRes val iconTextId: Int,
|
|
||||||
@StringRes val titleTextId: Int,
|
|
||||||
val key: NavKey,
|
|
||||||
)
|
|
||||||
@ -1,41 +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.navigation
|
|
||||||
|
|
||||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
|
||||||
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
|
||||||
import androidx.navigation3.ui.NavDisplay
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.toEntries
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
|
||||||
@Composable
|
|
||||||
fun NiaNavDisplay(
|
|
||||||
niaNavigator: NiaNavigator,
|
|
||||||
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
|
|
||||||
) {
|
|
||||||
val listDetailStrategy = rememberListDetailSceneStrategy<NiaNavKey>()
|
|
||||||
val entries = niaNavigator.niaNavigationState.toEntries(entryProviderBuilders)
|
|
||||||
NavDisplay(
|
|
||||||
entries = entries,
|
|
||||||
sceneStrategy = listDetailStrategy,
|
|
||||||
onBack = { niaNavigator.pop() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for the top level navigation items in the application. Contains UI information about the
|
||||||
|
* current route that is used in the top app bar and common navigation UI.
|
||||||
|
*
|
||||||
|
* @param selectedIcon The icon to be displayed in the navigation UI when this destination is
|
||||||
|
* selected.
|
||||||
|
* @param unselectedIcon The icon to be displayed in the navigation UI when this destination is
|
||||||
|
* not selected.
|
||||||
|
* @param iconTextId Text that to be displayed in the navigation UI.
|
||||||
|
* @param titleTextId Text that is displayed on the top app bar.
|
||||||
|
*/
|
||||||
|
data class TopLevelNavItem(
|
||||||
|
val selectedIcon: ImageVector,
|
||||||
|
val unselectedIcon: ImageVector,
|
||||||
|
@StringRes val iconTextId: Int,
|
||||||
|
@StringRes val titleTextId: Int,
|
||||||
|
)
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.navigation
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.R
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR
|
||||||
|
|
||||||
|
val FOR_YOU = TopLevelNavItem(
|
||||||
|
selectedIcon = NiaIcons.Upcoming,
|
||||||
|
unselectedIcon = NiaIcons.UpcomingBorder,
|
||||||
|
iconTextId = forYouR.string.feature_foryou_api_title,
|
||||||
|
titleTextId = R.string.app_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
val BOOKMARKS = TopLevelNavItem(
|
||||||
|
selectedIcon = NiaIcons.Bookmarks,
|
||||||
|
unselectedIcon = NiaIcons.BookmarksBorder,
|
||||||
|
iconTextId = bookmarksR.string.feature_bookmarks_api_title,
|
||||||
|
titleTextId = bookmarksR.string.feature_bookmarks_api_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
val INTERESTS = TopLevelNavItem(
|
||||||
|
selectedIcon = NiaIcons.Grid3x3,
|
||||||
|
unselectedIcon = NiaIcons.Grid3x3,
|
||||||
|
iconTextId = searchR.string.feature_search_api_interests,
|
||||||
|
titleTextId = searchR.string.feature_search_api_interests,
|
||||||
|
)
|
||||||
|
|
||||||
|
val TOP_LEVEL_ROUTES = mapOf(
|
||||||
|
ForYouRoute to FOR_YOU,
|
||||||
|
BookmarksRoute to BOOKMARKS,
|
||||||
|
InterestsRoute(null) to INTERESTS,
|
||||||
|
)
|
||||||
@ -1,253 +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.remember
|
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.createSavedStateHandle
|
|
||||||
import androidx.lifecycle.viewmodel.testing.ViewModelScenario
|
|
||||||
import androidx.lifecycle.viewmodel.testing.viewModelScenario
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
|
||||||
import kotlinx.serialization.modules.polymorphic
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class NiaBackStackViewModelTest {
|
|
||||||
|
|
||||||
@get:Rule val rule = createComposeRule()
|
|
||||||
|
|
||||||
private val serializersModules = SerializersModule {
|
|
||||||
polymorphic(NiaNavKey::class) {
|
|
||||||
subclass(TestStartKey::class, TestStartKey.serializer())
|
|
||||||
subclass(TestTopLevelKeyFirst::class, TestTopLevelKeyFirst.serializer())
|
|
||||||
subclass(TestTopLevelKeySecond::class, TestTopLevelKeySecond.serializer())
|
|
||||||
subclass(TestKeyFirst::class, TestKeyFirst.serializer())
|
|
||||||
subclass(TestKeySecond::class, TestKeySecond.serializer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createViewModel() = NiaBackStackViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
|
||||||
niaNavigationState = NiaNavigationState(TestStartKey),
|
|
||||||
serializersModules = serializersModules,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testStartKeySaved() {
|
|
||||||
rule.setContent {
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
assertThat(viewModel.backStackMap.size).isEqualTo(1)
|
|
||||||
val entry = viewModel.backStackMap[TestStartKey]
|
|
||||||
assertThat(entry).isNotNull()
|
|
||||||
assertThat(entry).containsExactly(TestStartKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testNonTopLevelKeySaved() {
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
rule.setContent {
|
|
||||||
val navigator = remember { NiaNavigator(viewModel.niaNavigationState) }
|
|
||||||
navigator.navigate(TestKeyFirst)
|
|
||||||
}
|
|
||||||
assertThat(viewModel.backStackMap.size).isEqualTo(1)
|
|
||||||
val entry = viewModel.backStackMap[TestStartKey]
|
|
||||||
assertThat(entry).isNotNull()
|
|
||||||
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testTopLevelKeySaved() {
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
rule.setContent {
|
|
||||||
val navigator = remember { NiaNavigator(viewModel.niaNavigationState) }
|
|
||||||
|
|
||||||
navigator.navigate(TestKeyFirst)
|
|
||||||
navigator.navigate(TestTopLevelKeyFirst)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(viewModel.backStackMap.size).isEqualTo(2)
|
|
||||||
|
|
||||||
val entry = viewModel.backStackMap[TestStartKey]
|
|
||||||
assertThat(entry).isNotNull()
|
|
||||||
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
|
|
||||||
|
|
||||||
val entry2 = viewModel.backStackMap[TestTopLevelKeyFirst]
|
|
||||||
assertThat(entry2).isNotNull()
|
|
||||||
assertThat(entry2).containsExactly(TestTopLevelKeyFirst)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMultiStacksSaved() {
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
rule.setContent {
|
|
||||||
val navigator = remember { NiaNavigator(viewModel.niaNavigationState) }
|
|
||||||
navigator.navigate(TestKeyFirst)
|
|
||||||
navigator.navigate(TestTopLevelKeyFirst)
|
|
||||||
navigator.navigate(TestKeySecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(viewModel.backStackMap.size).isEqualTo(2)
|
|
||||||
|
|
||||||
val entry = viewModel.backStackMap[TestStartKey]
|
|
||||||
assertThat(entry).isNotNull()
|
|
||||||
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
|
|
||||||
|
|
||||||
val entry2 = viewModel.backStackMap[TestTopLevelKeyFirst]
|
|
||||||
assertThat(entry2).isNotNull()
|
|
||||||
assertThat(entry2).containsExactly(TestTopLevelKeyFirst, TestKeySecond).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testPopSaved() {
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
rule.setContent {
|
|
||||||
val navigator = remember { NiaNavigator(viewModel.niaNavigationState) }
|
|
||||||
|
|
||||||
navigator.navigate(TestKeyFirst)
|
|
||||||
|
|
||||||
assertThat(viewModel.backStackMap.size).isEqualTo(1)
|
|
||||||
val entry = viewModel.backStackMap[TestStartKey]
|
|
||||||
assertThat(entry).isNotNull()
|
|
||||||
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
|
|
||||||
|
|
||||||
navigator.pop()
|
|
||||||
|
|
||||||
assertThat(viewModel.backStackMap.size).isEqualTo(1)
|
|
||||||
val entry2 = viewModel.backStackMap[TestStartKey]
|
|
||||||
assertThat(entry2).isNotNull()
|
|
||||||
assertThat(entry2).containsExactly(TestStartKey).inOrder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestore() {
|
|
||||||
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
|
|
||||||
lateinit var navigator: NiaNavigator
|
|
||||||
lateinit var navigatorState: NiaNavigationState
|
|
||||||
rule.setContent {
|
|
||||||
navigatorState = remember { NiaNavigationState(TestStartKey) }
|
|
||||||
navigator = remember { NiaNavigator(navigatorState) }
|
|
||||||
scenario = viewModelScenario {
|
|
||||||
NiaBackStackViewModel(
|
|
||||||
savedStateHandle = createSavedStateHandle(),
|
|
||||||
niaNavigationState = navigatorState,
|
|
||||||
serializersModules = serializersModules,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.runOnIdle {
|
|
||||||
navigator.navigate(TestKeyFirst)
|
|
||||||
assertThat(navigatorState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario.recreate()
|
|
||||||
|
|
||||||
rule.runOnIdle {
|
|
||||||
assertThat(navigatorState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestoreMultiStacks() {
|
|
||||||
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
|
|
||||||
lateinit var navigator: NiaNavigator
|
|
||||||
lateinit var navigatorState: NiaNavigationState
|
|
||||||
rule.setContent {
|
|
||||||
navigatorState = remember { NiaNavigationState(TestStartKey) }
|
|
||||||
navigator = remember { NiaNavigator(navigatorState) }
|
|
||||||
scenario = viewModelScenario {
|
|
||||||
NiaBackStackViewModel(
|
|
||||||
savedStateHandle = createSavedStateHandle(),
|
|
||||||
niaNavigationState = navigatorState,
|
|
||||||
serializersModules = serializersModules,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.runOnIdle {
|
|
||||||
navigator.navigate(TestKeyFirst)
|
|
||||||
navigator.navigate(TestTopLevelKeyFirst)
|
|
||||||
navigator.navigate(TestKeySecond)
|
|
||||||
|
|
||||||
assertThat(navigatorState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
TestTopLevelKeyFirst,
|
|
||||||
TestKeySecond,
|
|
||||||
).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario.recreate()
|
|
||||||
|
|
||||||
rule.runOnIdle {
|
|
||||||
assertThat(navigatorState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
TestTopLevelKeyFirst,
|
|
||||||
TestKeySecond,
|
|
||||||
).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private object TestStartKey : NiaNavKey {
|
|
||||||
override val isTopLevel: Boolean
|
|
||||||
get() = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private object TestTopLevelKeyFirst : NiaNavKey {
|
|
||||||
override val isTopLevel: Boolean
|
|
||||||
get() = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private object TestTopLevelKeySecond : NiaNavKey {
|
|
||||||
override val isTopLevel: Boolean
|
|
||||||
get() = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private object TestKeyFirst : NiaNavKey {
|
|
||||||
override val isTopLevel: Boolean
|
|
||||||
get() = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private object TestKeySecond : NiaNavKey {
|
|
||||||
override val isTopLevel: Boolean
|
|
||||||
get() = false
|
|
||||||
}
|
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation3.runtime.NavKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles navigation events (forward and back) by updating the navigation state.
|
||||||
|
*
|
||||||
|
* @param state - The navigation state that will be updated in response to navigation events.
|
||||||
|
*/
|
||||||
|
class Navigator(val state: NavigationState) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a navigation key
|
||||||
|
*
|
||||||
|
* @param key - the navigation key to navigate to.
|
||||||
|
*/
|
||||||
|
fun navigate(key: NavKey) {
|
||||||
|
when (key) {
|
||||||
|
state.currentTopLevelKey -> goUp()
|
||||||
|
in state.topLevelKeys -> goToTopLevel(key)
|
||||||
|
else -> goToKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go back to the previous navigation key.
|
||||||
|
*/
|
||||||
|
fun goBack() {
|
||||||
|
when (state.currentKey){
|
||||||
|
state.startKey -> error("You cannot go back from the start route")
|
||||||
|
state.currentTopLevelKey -> {
|
||||||
|
// We're at the base of the current sub stack, go back to the previous top level
|
||||||
|
// stack.
|
||||||
|
state.topLevelStack.removeLastOrNull()
|
||||||
|
}
|
||||||
|
else -> state.currentSubStack.removeLastOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to a non top level key.
|
||||||
|
*/
|
||||||
|
private fun goToKey(key: NavKey) {
|
||||||
|
state.currentSubStack.apply {
|
||||||
|
// Remove it if it's already in the stack so it's added at the end.
|
||||||
|
remove(key)
|
||||||
|
add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to a top level stack.
|
||||||
|
*/
|
||||||
|
private fun goToTopLevel(key: NavKey) {
|
||||||
|
state.topLevelStack.apply {
|
||||||
|
if (key == state.startKey) {
|
||||||
|
// This is the start key. Clear the stack so it's added as the only key.
|
||||||
|
clear()
|
||||||
|
} else {
|
||||||
|
// Remove it if it's already in the stack so it's added at the end.
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go up in the current sub stack by clearing all but the root key.
|
||||||
|
*/
|
||||||
|
private fun goUp() {
|
||||||
|
state.currentSubStack.run {
|
||||||
|
if (size > 1) subList(1, size).clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,90 +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.annotation.VisibleForTesting
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.serialization.saved
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.savedstate.serialization.SavedStateConfiguration
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import kotlinx.serialization.builtins.MapSerializer
|
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
|
||||||
import kotlinx.serialization.serializer
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: I'm not sure why this needs to be a ViewModel - why can't it be a plain state holder that
|
|
||||||
* is scoped to `NiaAppState`?
|
|
||||||
* https://github.com/android/nav3-recipes/blob/main/app/src/main/java/com/example/nav3recipes/multiplestacks/NavigationState.kt#L71
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
@HiltViewModel
|
|
||||||
class NiaBackStackViewModel @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
val niaNavigationState: NiaNavigationState,
|
|
||||||
serializersModules: SerializersModule,
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val config = SavedStateConfiguration { serializersModule = serializersModules }
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal var backStackMap by savedStateHandle.saved(
|
|
||||||
serializer = MapSerializer(
|
|
||||||
serializer<NiaNavKey>(),
|
|
||||||
serializer<List<NiaNavKey>>(),
|
|
||||||
),
|
|
||||||
configuration = config,
|
|
||||||
) {
|
|
||||||
linkedMapOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal var activeTopLeveLKeys by savedStateHandle.saved(
|
|
||||||
serializer = ListSerializer(serializer<NiaNavKey>()),
|
|
||||||
configuration = config,
|
|
||||||
) {
|
|
||||||
listOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (backStackMap.isNotEmpty()) {
|
|
||||||
// Restore backstack from saved state handle if not empty
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
niaNavigationState.restore(
|
|
||||||
activeTopLeveLKeys,
|
|
||||||
backStackMap as LinkedHashMap<NiaNavKey, SnapshotStateList<NiaNavKey>>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start observing changes to the backStack and save backStack whenever it updates
|
|
||||||
viewModelScope.launch {
|
|
||||||
snapshotFlow {
|
|
||||||
activeTopLeveLKeys = niaNavigationState.activeTopLeveLKeys.toList()
|
|
||||||
backStackMap = niaNavigationState.backStacks
|
|
||||||
}.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@ -1,173 +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.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
|
|
||||||
|
|
||||||
// TODO: Consider changing this to `NiaNavigationState`
|
|
||||||
class NiaNavigationState(
|
|
||||||
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] ?: error("No back stack found for $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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Document this
|
|
||||||
*/
|
|
||||||
class NiaNavigator @Inject constructor(
|
|
||||||
val niaNavigationState: NiaNavigationState,
|
|
||||||
) {
|
|
||||||
// TODO: I wonder if it'd be simpler to have separate methods
|
|
||||||
// for navigating to a graph and navigating to a key. If the key is on a separate graph then
|
|
||||||
// navigate to that graph first.
|
|
||||||
fun navigate(key: NiaNavKey) {
|
|
||||||
val currentActiveSubStacks = linkedSetOf<NiaNavKey>()
|
|
||||||
niaNavigationState.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() {
|
|
||||||
niaNavigationState.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: I wonder if removing this would remove the need for the serializers modules
|
|
||||||
interface NiaNavKey {
|
|
||||||
val isTopLevel: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the navigation state to `NavEntry`s that can be displayed by a `NavDisplay`
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun NiaNavigationState.toEntries(
|
|
||||||
// TODO: Might be better to pass this in fully constructed
|
|
||||||
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,46 +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
|
|
||||||
*
|
|
||||||
* 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.google.samples.apps.nowinandroid.core.navigation.simple
|
|
||||||
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles navigation events (forward and back) by updating the navigation state.
|
|
||||||
*/
|
|
||||||
class Navigator(val state: NavigationState){
|
|
||||||
fun navigate(route: NavKey){
|
|
||||||
if (route in state.backStacks.keys){
|
|
||||||
// This is a top level route, just switch to it
|
|
||||||
state.topLevelRoute = route
|
|
||||||
} else {
|
|
||||||
state.backStacks[state.topLevelRoute]?.add(route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun goBack(){
|
|
||||||
val currentStack = state.backStacks[state.topLevelRoute] ?:
|
|
||||||
error("Stack for ${state.topLevelRoute} not found")
|
|
||||||
val currentRoute = currentStack.last()
|
|
||||||
|
|
||||||
// If we're at the base of the current route, go back to the start route stack.
|
|
||||||
if (currentRoute == state.topLevelRoute){
|
|
||||||
state.topLevelRoute = state.startRoute
|
|
||||||
} else {
|
|
||||||
currentStack.removeLastOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation3.runtime.NavBackStack
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
private object TestFirstTopLevelKey : NavKey
|
||||||
|
private object TestSecondTopLevelKey : NavKey
|
||||||
|
private object TestThirdTopLevelKey : NavKey
|
||||||
|
private object TestKeyFirst : NavKey
|
||||||
|
private object TestKeySecond : NavKey
|
||||||
|
|
||||||
|
class NavigatorTest {
|
||||||
|
|
||||||
|
private lateinit var navigationState: NavigationState
|
||||||
|
private lateinit var navigator: Navigator
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
|
||||||
|
val startKey = TestFirstTopLevelKey
|
||||||
|
val topLevelStack = NavBackStack<NavKey>(startKey)
|
||||||
|
val topLevelKeys = listOf(
|
||||||
|
startKey,
|
||||||
|
TestSecondTopLevelKey,
|
||||||
|
TestThirdTopLevelKey
|
||||||
|
)
|
||||||
|
val subStacks = topLevelKeys.associateWith { key -> NavBackStack(key) }
|
||||||
|
|
||||||
|
navigationState = NavigationState(
|
||||||
|
startKey = startKey,
|
||||||
|
topLevelStack = topLevelStack,
|
||||||
|
subStacks = subStacks
|
||||||
|
)
|
||||||
|
navigator = Navigator(navigationState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStartKey() {
|
||||||
|
assertThat(navigationState.startKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNavigate() {
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
assertThat(navigationState.subStacks[TestFirstTopLevelKey]?.last()).isEqualTo(TestKeyFirst)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNavigateTopLevel() {
|
||||||
|
navigator.navigate(TestSecondTopLevelKey)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNavigateSingleTop() {
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
).inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNavigateTopLevelSingleTop() {
|
||||||
|
navigator.navigate(TestSecondTopLevelKey)
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestSecondTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
navigator.navigate(TestSecondTopLevelKey)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestSecondTopLevelKey,
|
||||||
|
).inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSubStack() {
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
|
||||||
|
navigator.navigate(TestKeySecond)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestKeySecond)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMultiStack() {
|
||||||
|
// add to start stack
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
|
||||||
|
// navigate to new top level
|
||||||
|
navigator.navigate(TestSecondTopLevelKey)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||||
|
|
||||||
|
// add to new stack
|
||||||
|
navigator.navigate(TestKeySecond)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestKeySecond)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||||
|
|
||||||
|
// go back to start stack
|
||||||
|
navigator.navigate(TestFirstTopLevelKey)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPopOneNonTopLevel() {
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
navigator.navigate(TestKeySecond)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
TestKeySecond,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
navigator.goBack()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPopOneTopLevel() {
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
navigator.navigate(TestSecondTopLevelKey)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestSecondTopLevelKey,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||||
|
|
||||||
|
// remove TopLevel
|
||||||
|
navigator.goBack()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun popMultipleNonTopLevel() {
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
navigator.navigate(TestKeySecond)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
TestKeySecond,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
navigator.goBack()
|
||||||
|
navigator.goBack()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun popMultipleTopLevel() {
|
||||||
|
// second sub-stack
|
||||||
|
navigator.navigate(TestSecondTopLevelKey)
|
||||||
|
navigator.navigate(TestKeyFirst)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestSecondTopLevelKey,
|
||||||
|
TestKeyFirst,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
// third sub-stack
|
||||||
|
navigator.navigate(TestThirdTopLevelKey)
|
||||||
|
navigator.navigate(TestKeySecond)
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestThirdTopLevelKey,
|
||||||
|
TestKeySecond,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
repeat(4) {
|
||||||
|
navigator.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(navigationState.currentSubStack).containsExactly(
|
||||||
|
TestFirstTopLevelKey,
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun throwOnEmptyBackStack() {
|
||||||
|
assertFailsWith<IllegalStateException> {
|
||||||
|
navigator.goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,287 +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.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 niaNavigationState: NiaNavigationState
|
|
||||||
private lateinit var niaNavigator: NiaNavigator
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
niaNavigationState = NiaNavigationState(TestStartKey)
|
|
||||||
niaNavigator = NiaNavigator(niaNavigationState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testStartKey() {
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestStartKey)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testNavigate() {
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testNavigateTopLevel() {
|
|
||||||
niaNavigator.navigate(TestTopLevelKey)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestTopLevelKey)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testNavigateSingleTop() {
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testNavigateTopLevelSingleTop() {
|
|
||||||
niaNavigator.navigate(TestTopLevelKey)
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestTopLevelKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
niaNavigator.navigate(TestTopLevelKey)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestTopLevelKey,
|
|
||||||
).inOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testSubStack() {
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
|
|
||||||
niaNavigator.navigate(TestKeySecond)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeySecond)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMultiStack() {
|
|
||||||
// add to start stack
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
|
|
||||||
// navigate to new top level
|
|
||||||
niaNavigator.navigate(TestTopLevelKey)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestTopLevelKey)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
|
||||||
|
|
||||||
// add to new stack
|
|
||||||
niaNavigator.navigate(TestKeySecond)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeySecond)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
|
||||||
|
|
||||||
// go back to start stack
|
|
||||||
niaNavigator.navigate(TestStartKey)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestore() {
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(TestStartKey)
|
|
||||||
|
|
||||||
niaNavigationState.restore(
|
|
||||||
listOf(TestStartKey, TestTopLevelKey),
|
|
||||||
linkedMapOf(
|
|
||||||
TestStartKey to mutableStateListOf(TestStartKey, TestKeyFirst),
|
|
||||||
TestTopLevelKey to mutableStateListOf(TestTopLevelKey, TestKeySecond),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
TestTopLevelKey,
|
|
||||||
TestKeySecond,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeySecond)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testPopOneNonTopLevel() {
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
niaNavigator.navigate(TestKeySecond)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
TestKeySecond,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
niaNavigator.pop()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testPopOneTopLevel() {
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
niaNavigator.navigate(TestTopLevelKey)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
TestTopLevelKey,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestTopLevelKey)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
|
||||||
|
|
||||||
// remove TopLevel
|
|
||||||
niaNavigator.pop()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst)
|
|
||||||
assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun popMultipleNonTopLevel() {
|
|
||||||
niaNavigator.navigate(TestKeyFirst)
|
|
||||||
niaNavigator.navigate(TestKeySecond)
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
TestKeySecond,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
niaNavigator.pop()
|
|
||||||
niaNavigator.pop()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestStartKey)
|
|
||||||
assertThat(niaNavigationState.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(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
TestTopLevelKey,
|
|
||||||
TestKeyFirst,
|
|
||||||
testTopLevelKeyTwo,
|
|
||||||
TestKeySecond,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
repeat(4) {
|
|
||||||
niaNavigator.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentBackStack).containsExactly(
|
|
||||||
TestStartKey,
|
|
||||||
).inOrder()
|
|
||||||
|
|
||||||
assertThat(niaNavigationState.currentKey).isEqualTo(TestStartKey)
|
|
||||||
assertThat(niaNavigationState.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
|
|
||||||
}
|
|
||||||
@ -1,42 +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.feature.bookmarks.impl.navigation
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import dagger.multibindings.IntoSet
|
|
||||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object BookmarksSerializerModule {
|
|
||||||
@Provides
|
|
||||||
@IntoSet
|
|
||||||
fun provideBookmarksPolymorphicModuleBuilder(): PolymorphicModuleBuilder<@JvmSuppressWildcards NiaNavKey>.() -> Unit = {
|
|
||||||
subclass(BookmarksRoute::class, BookmarksRoute.serializer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@ -1,42 +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.feature.foryou.impl.navigation
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import dagger.multibindings.IntoSet
|
|
||||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object ForYouRouteSerializerModule {
|
|
||||||
@Provides
|
|
||||||
@IntoSet
|
|
||||||
fun provideForYouPolymorphicModuleBuilder(): PolymorphicModuleBuilder<@JvmSuppressWildcards NiaNavKey>.() -> Unit = {
|
|
||||||
subclass(ForYouRoute::class, ForYouRoute.serializer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@ -1,42 +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.feature.interests.impl.navigation
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import dagger.multibindings.IntoSet
|
|
||||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object InterestsSerializerModule {
|
|
||||||
@Provides
|
|
||||||
@IntoSet
|
|
||||||
fun provideInterestsPolymorphicModuleBuilder(): PolymorphicModuleBuilder<@JvmSuppressWildcards NiaNavKey>.() -> Unit = {
|
|
||||||
subclass(InterestsRoute::class, InterestsRoute.serializer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@ -1,40 +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.feature.search.impl.navigation
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRoute
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import dagger.multibindings.IntoSet
|
|
||||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/*@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object SearchSerializerModule {
|
|
||||||
@Provides
|
|
||||||
@IntoSet
|
|
||||||
fun provideSearchPolymorphicModuleBuilder(): PolymorphicModuleBuilder<@JvmSuppressWildcards NiaNavKey>.() -> Unit = {
|
|
||||||
subclass(SearchRoute::class, SearchRoute.serializer())
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
@ -1,39 +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.feature.topic.api.navigation
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import dagger.multibindings.IntoSet
|
|
||||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/*@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object TopicSerializerModule {
|
|
||||||
@Provides
|
|
||||||
@IntoSet
|
|
||||||
fun provideTopicPolymorphicModuleBuilder(): PolymorphicModuleBuilder<@JvmSuppressWildcards NiaNavKey>.() -> Unit = {
|
|
||||||
subclass(TopicRoute::class, TopicRoute.serializer())
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
Loading…
Reference in new issue