diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/NavigationStateProvider.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/NavigationStateProvider.kt deleted file mode 100644 index 89c4943fa..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/NavigationStateProvider.kt +++ /dev/null @@ -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.() -> Unit>, - ): SerializersModule = SerializersModule { - polymorphic(NiaNavKey::class) { - polymorphicModuleBuilders.forEach { it() } - } - } -} -*/ diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NavBarItem.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NavBarItem.kt deleted file mode 100644 index b4d42e761..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NavBarItem.kt +++ /dev/null @@ -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( - 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, -) diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavDisplay.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavDisplay.kt deleted file mode 100644 index 562b3a791..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavDisplay.kt +++ /dev/null @@ -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.() -> Unit>, -) { - val listDetailStrategy = rememberListDetailSceneStrategy() - val entries = niaNavigator.niaNavigationState.toEntries(entryProviderBuilders) - NavDisplay( - entries = entries, - sceneStrategy = listDetailStrategy, - onBack = { niaNavigator.pop() }, - ) -} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt new file mode 100644 index 000000000..c3a90219d --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelRoutes.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelRoutes.kt new file mode 100644 index 000000000..c82544b8f --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelRoutes.kt @@ -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, +) diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 3439487b2..2dacde653 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -72,8 +72,8 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAp import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors -import com.google.samples.apps.nowinandroid.core.navigation.simple.Navigator -import com.google.samples.apps.nowinandroid.core.navigation.simple.toEntries +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.toEntries import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYouEntry @@ -92,7 +92,7 @@ fun NiaApp( modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { - val shouldShowGradientBackground = appState.currentNavBarItem == FOR_YOU + val shouldShowGradientBackground = appState.currentTopLevelNavItem == FOR_YOU var showSettingsDialog by rememberSaveable { mutableStateOf(false) } NiaBackground(modifier = modifier) { @@ -145,7 +145,7 @@ internal fun NiaApp( modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { - val unreadDestinations by appState.topLevelDestinationsWithUnreadResources + val unreadRoutes by appState.topLevelRoutesWithUnreadResources .collectAsStateWithLifecycle() if (showSettingsDialog) { @@ -160,25 +160,25 @@ internal fun NiaApp( NiaNavigationSuiteScaffold( navigationSuiteItems = { - TOP_LEVEL_ROUTES.forEach { (navKey, navBarItem) -> - val hasUnread = unreadDestinations.contains(navBarItem) - val selected = navKey == appState.navigationState.topLevelRoute + TOP_LEVEL_ROUTES.forEach { (route, navItem) -> + val hasUnread = unreadRoutes.contains(route) + val selected = route == appState.navigationState.currentTopLevelKey item( selected = selected, - onClick = { navigator.navigate(navKey) }, + onClick = { navigator.navigate(route) }, icon = { Icon( - imageVector = navBarItem.unselectedIcon, + imageVector = navItem.unselectedIcon, contentDescription = null, ) }, selectedIcon = { Icon( - imageVector = navBarItem.selectedIcon, + imageVector = navItem.selectedIcon, contentDescription = null, ) }, - label = { Text(stringResource(navBarItem.iconTextId)) }, + label = { Text(stringResource(navItem.iconTextId)) }, modifier = Modifier .testTag("NiaNavItem") .then(if (hasUnread) Modifier.notificationDot() else Modifier), @@ -217,7 +217,7 @@ internal fun NiaApp( ), ) { // Show the top app bar on top level destinations. - val destination = appState.currentNavBarItem + val destination = appState.currentTopLevelNavItem var shouldShowTopAppBar = false if (destination != null) { diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index 330725e4a..4bb8926ff 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -20,16 +20,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation3.runtime.NavKey import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor -import com.google.samples.apps.nowinandroid.core.navigation.simple.NavigationState -import com.google.samples.apps.nowinandroid.core.navigation.simple.rememberNavigationState +import com.google.samples.apps.nowinandroid.core.navigation.NavigationState +import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState +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.navigation.BOOKMARKS -import com.google.samples.apps.nowinandroid.navigation.FOR_YOU import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_ROUTES -import com.google.samples.apps.nowinandroid.navigation.NavBarItem +import com.google.samples.apps.nowinandroid.navigation.TopLevelNavItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -75,8 +75,9 @@ class NiaAppState( userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, ) { - val currentNavBarItem: NavBarItem? - @Composable get() = TOP_LEVEL_ROUTES[navigationState.topLevelRoute] + // TODO: I think this should return null if the current route is not a topLevelRoute + val currentTopLevelNavItem: TopLevelNavItem? + @Composable get() = TOP_LEVEL_ROUTES[navigationState.currentTopLevelKey] val isOffline = networkMonitor.isOnline .map(Boolean::not) @@ -87,14 +88,14 @@ class NiaAppState( ) /** - * The top level destinations that have unread news resources. + * The top level routes that have unread news resources. */ - val topLevelDestinationsWithUnreadResources: StateFlow> = + val topLevelRoutesWithUnreadResources: StateFlow> = userNewsResourceRepository.observeAllForFollowedTopics() .combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources -> setOfNotNull( - FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, - BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, + ForYouRoute.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, + BookmarksRoute.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, ) } .stateIn( diff --git a/core/navigation/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModelTest.kt b/core/navigation/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModelTest.kt deleted file mode 100644 index d0c1006ac..000000000 --- a/core/navigation/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModelTest.kt +++ /dev/null @@ -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 - 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 - 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 -} diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/NavigationState.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt similarity index 57% rename from core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/NavigationState.kt rename to core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt index 0e84aefef..a3dbd62de 100644 --- a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/NavigationState.kt +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,15 +14,13 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.navigation.simple +package com.google.samples.apps.nowinandroid.core.navigation +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSerializable -import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.navigation3.runtime.NavBackStack @@ -31,32 +29,24 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.runtime.serialization.NavKeySerializer -import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer /** * Create a navigation state that persists config changes and process death. */ @Composable fun rememberNavigationState( - startRoute: NavKey, - topLevelRoutes: Set + startKey: NavKey, + topLevelKeys: Set ): NavigationState { - val topLevelRoute = rememberSerializable( - startRoute, topLevelRoutes, - serializer = MutableStateSerializer(NavKeySerializer()) - ) { - mutableStateOf(startRoute) - } - - val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } + val topLevelStack = rememberNavBackStack(startKey) + val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) } - return remember(startRoute, topLevelRoutes) { + return remember(startKey, topLevelKeys) { NavigationState( - startRoute = startRoute, - topLevelRoute = topLevelRoute, - backStacks = backStacks + startKey = startKey, + topLevelStack = topLevelStack, + subStacks = subStacks ) } } @@ -64,23 +54,37 @@ fun rememberNavigationState( /** * State holder for navigation state. * - * @param startRoute - the start route. The user will exit the app through this route. - * @param topLevelRoute - the current top level route - * @param backStacks - the back stacks for each top level route + * @param startKey - the starting navigation key. The user will exit the app through this key. + * @param currentTopLevelKey - the current top level key + * @param subStacks - the back stacks for each top level key */ class NavigationState( - val startRoute: NavKey, - topLevelRoute: MutableState, - val backStacks: Map> + val startKey: NavKey, + val topLevelStack: NavBackStack, + val subStacks: Map> ) { - var topLevelRoute: NavKey by topLevelRoute - val stacksInUse: List - get() = if (topLevelRoute == startRoute) { + val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() } + + val topLevelKeys + get() = subStacks.keys + + @get:VisibleForTesting + val currentSubStack : NavBackStack + get() = subStacks[currentTopLevelKey] + ?: error("Sub stack for $currentTopLevelKey does not exist") + + @get:VisibleForTesting + val currentKey + get() = currentSubStack?.last() + + // TODO: Fix this + /*val stacksInUse: List + get() = topLevel listOf(startRoute) } else { listOf(startRoute, topLevelRoute) } - +*/ } /** @@ -91,7 +95,7 @@ fun NavigationState.toEntries( entryProvider: (NavKey) -> NavEntry ): SnapshotStateList> { - val decoratedEntries = backStacks.mapValues { (_, stack) -> + val decoratedEntries = subStacks.mapValues { (_, stack) -> val decorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), ) @@ -102,7 +106,7 @@ fun NavigationState.toEntries( ) } - return stacksInUse + return topLevelStack .flatMap { decoratedEntries[it] ?: emptyList() } .toMutableStateList() } \ No newline at end of file diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt new file mode 100644 index 000000000..8851b0dca --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModel.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModel.kt deleted file mode 100644 index 323c90375..000000000 --- a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModel.kt +++ /dev/null @@ -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(), - serializer>(), - ), - configuration = config, - ) { - linkedMapOf() - } - - @VisibleForTesting - internal var activeTopLeveLKeys by savedStateHandle.saved( - serializer = ListSerializer(serializer()), - 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>, - ) - } - - // Start observing changes to the backStack and save backStack whenever it updates - viewModelScope.launch { - snapshotFlow { - activeTopLeveLKeys = niaNavigationState.activeTopLeveLKeys.toList() - backStackMap = niaNavigationState.backStacks - }.collect() - } - } -} -*/ diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigator.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigator.kt deleted file mode 100644 index 4e4659678..000000000 --- a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigator.kt +++ /dev/null @@ -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> = - linkedMapOf( - startKey to mutableStateListOf(startKey), - ) - - val activeTopLeveLKeys: SnapshotStateList = mutableStateListOf(startKey) - - var currentTopLevelKey: NiaNavKey by mutableStateOf(activeTopLeveLKeys.last()) - private set - - @get:VisibleForTesting - val currentBackStack: List - 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) { - 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, map: LinkedHashMap>?) { - 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() - 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.() -> Unit>, -): List> = - activeTopLeveLKeys.fold(emptyList()) { entries, topLevelKey -> - val decorated = key(topLevelKey) { - val decorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - ) - rememberDecoratedNavEntries( - backStack = backStacks[topLevelKey]!!, - entryDecorators = decorators, - entryProvider = entryProvider { - entryProviderBuilders.forEach { builder -> - builder() - } - }, - ) - } - entries + decorated - } diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/Navigator.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/Navigator.kt deleted file mode 100644 index fbbff572a..000000000 --- a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/Navigator.kt +++ /dev/null @@ -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() - } - } -} diff --git a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt new file mode 100644 index 000000000..05b6544d4 --- /dev/null +++ b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt @@ -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(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 { + navigator.goBack() + } + } +} + + diff --git a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorStateTest.kt b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorStateTest.kt deleted file mode 100644 index 4b2342122..000000000 --- a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorStateTest.kt +++ /dev/null @@ -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 { - 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 -} diff --git a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt index 9a4fc3cd1..9e9d25bf1 100644 --- a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt @@ -22,33 +22,16 @@ import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.runtime.compositionLocalOf import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import com.google.samples.apps.nowinandroid.core.navigation.simple.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksScreen -import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute -//import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(ActivityComponent::class) -object BookmarksEntryProvider { - - @Provides - @IntoSet - fun provideBookmarksEntryProviderBuilder( - navigator: Navigator, - ): EntryProviderScope.() -> Unit = { bookmarksEntry(navigator) } -} +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic fun EntryProviderScope.bookmarksEntry(navigator: Navigator) { entry { val snackbarHostState = LocalSnackbarHostState.current BookmarksScreen( - onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + onTopicClick = navigator::navigateToTopic, onShowSnackbar = { message, action -> snackbarHostState.showSnackbar( message = message, diff --git a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksSerializerModule.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksSerializerModule.kt deleted file mode 100644 index 36390833b..000000000 --- a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksSerializerModule.kt +++ /dev/null @@ -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()) - } -} -*/ diff --git a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt index 7c8133bd0..d2aefa0f8 100644 --- a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt @@ -18,35 +18,15 @@ package com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import com.google.samples.apps.nowinandroid.core.navigation.simple.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen -import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute -//import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(ActivityComponent::class) -object ForYouEntryProvider { - /** - * The ForYou composable for the app. It can also display information about topics. - * This should be supplied from a separate module. - */ - @Provides - @IntoSet - fun provideForYouEntryProviderBuilder( - navigator: Navigator, - ): EntryProviderScope.() -> Unit = { forYouEntry(navigator) } -} +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic fun EntryProviderScope.forYouEntry(navigator: Navigator) { entry { ForYouScreen( - onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + onTopicClick = navigator::navigateToTopic, ) } } diff --git a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouRouteSerializerModule.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouRouteSerializerModule.kt deleted file mode 100644 index 4d6cbaf5d..000000000 --- a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouRouteSerializerModule.kt +++ /dev/null @@ -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()) - } -} -*/ diff --git a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt index e35642a26..4aa12ef86 100644 --- a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt @@ -21,12 +21,13 @@ import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import com.google.samples.apps.nowinandroid.core.navigation.simple.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsDetailPlaceholder import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsScreen import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic //import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic import dagger.Module import dagger.Provides @@ -57,9 +58,8 @@ fun EntryProviderScope.interestsEntry(navigator: Navigator) { it.create(key) } InterestsScreen( - // TODO: This event should be provided by the ViewModel - // TODO: This could be made into an extension function on navigator - onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + // TODO: This event should either be provided by the ViewModel or by the navigator, not both + onTopicClick = navigator::navigateToTopic, // TODO: This should be dynamically calculated based on the rendering scene // See https://github.com/android/nav3-recipes/commit/488f4811791ca3ed7192f4fe3c86e7371b32ebdc#diff-374e02026cdd2f68057dd940f203dc4ba7319930b33e9555c61af7e072211cabR89 diff --git a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsSerializerModule.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsSerializerModule.kt deleted file mode 100644 index 03e9b0437..000000000 --- a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsSerializerModule.kt +++ /dev/null @@ -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()) - } -} -*/ diff --git a/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt index 2606a748f..5007dbd9d 100644 --- a/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt +++ b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt @@ -17,21 +17,7 @@ package com.google.samples.apps.nowinandroid.feature.search.api.navigation import androidx.navigation3.runtime.NavKey -import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey -import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator import kotlinx.serialization.Serializable -/* -@Serializable -object SearchRoute : NiaNavKey { - override val isTopLevel: Boolean - get() = false -} - -fun NiaNavigator.navigateToSearch() { - navigate(SearchRoute) -} -*/ - @Serializable object SearchRoute : NavKey diff --git a/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt index 1fba2ca83..f4df9f34a 100644 --- a/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt @@ -18,34 +18,18 @@ package com.google.samples.apps.nowinandroid.feature.search.impl.navigation import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import com.google.samples.apps.nowinandroid.core.navigation.simple.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRoute import com.google.samples.apps.nowinandroid.feature.search.impl.SearchScreen -import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(ActivityComponent::class) -object SearchEntryProvider { - - @Provides - @IntoSet - fun provideSearchEntryProviderBuilder( - navigator: Navigator, - ): EntryProviderScope.() -> Unit = { searchEntry(navigator) } -} +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic fun EntryProviderScope.searchEntry(navigator: Navigator) { - entry { key -> + entry { SearchScreen( onBackClick = { navigator.goBack() }, onInterestsClick = { navigator.navigate(InterestsRoute()) }, - onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + onTopicClick = navigator::navigateToTopic, ) } } diff --git a/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchSerializerModule.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchSerializerModule.kt deleted file mode 100644 index 6bf6a0e97..000000000 --- a/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchSerializerModule.kt +++ /dev/null @@ -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()) - } -}*/ diff --git a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt index a331aaf27..ebc6636e9 100644 --- a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt +++ b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt @@ -17,23 +17,14 @@ package com.google.samples.apps.nowinandroid.feature.topic.api.navigation import androidx.navigation3.runtime.NavKey -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.Navigator import kotlinx.serialization.Serializable -/* @Serializable -data class TopicRoute(val id: String) : NiaNavKey { - override val isTopLevel: Boolean - get() = false -} +data class TopicRoute(val id: String) : NavKey -fun NiaNavigator.navigateToTopic( +fun Navigator.navigateToTopic( topicId: String, ) { navigate(TopicRoute(topicId)) } -*/ - -@Serializable -data class TopicRoute(val id: String) : NavKey diff --git a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicSerializerModule.kt b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicSerializerModule.kt deleted file mode 100644 index 4bb36e2f0..000000000 --- a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicSerializerModule.kt +++ /dev/null @@ -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()) - } -}*/ diff --git a/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt index ec1bf7393..c49fdddfc 100644 --- a/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt @@ -21,29 +21,12 @@ import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import com.google.samples.apps.nowinandroid.core.navigation.simple.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute -//import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicScreen import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel.Factory -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(ActivityComponent::class) -object TopicEntryProvider { - - @OptIn(ExperimentalMaterial3AdaptiveApi::class) - @Provides - @IntoSet - fun provideTopicEntryProviderBuilder( - navigator: Navigator, - ): EntryProviderScope.() -> Unit = { topicEntry(navigator) } -} @OptIn(ExperimentalMaterial3AdaptiveApi::class) fun EntryProviderScope.topicEntry(navigator: Navigator) { @@ -54,7 +37,7 @@ fun EntryProviderScope.topicEntry(navigator: Navigator) { TopicScreen( showBackButton = true, onBackClick = { navigator.goBack() }, - onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + onTopicClick = navigator::navigateToTopic, viewModel = hiltViewModel( key = id, ) { factory ->