diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt index cc03c0058..1afff9b3f 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -33,6 +33,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.metrics.performance.JankStats import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey import androidx.tracing.trace import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper @@ -41,7 +42,7 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc 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.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackViewModel +//import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackViewModel 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.ui.LocalTimeZone @@ -77,17 +78,17 @@ class MainActivity : ComponentActivity() { @Inject lateinit var userNewsResourceRepository: UserNewsResourceRepository - @Inject - lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderScope.() -> Unit> - - @Inject - lateinit var niaNavigator: NiaNavigator + /*@Inject + lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderScope.() -> Unit> +*/ + /*@Inject + lateinit var niaNavigator: NiaNavigator*/ private val viewModel: MainActivityViewModel by viewModels() // TODO: This isn't used - private val backStackViewModel: NiaBackStackViewModel by viewModels() + //private val backStackViewModel: NiaBackStackViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() @@ -147,11 +148,12 @@ class MainActivity : ComponentActivity() { splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() } setContent { + val appState = rememberNiaAppState( networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, - niaNavigator = niaNavigator, + //niaNavigator = niaNavigator, ) val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle() @@ -166,8 +168,7 @@ class MainActivity : ComponentActivity() { disableDynamicTheming = themeSettings.disableDynamicTheming, ) { NiaApp( - appState, - entryProviderBuilders = entryProviderBuilders, + appState ) } } 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 index 0bb3886c8..00df4c287 100644 --- 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 @@ -17,7 +17,7 @@ package com.google.samples.apps.nowinandroid.di import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey -import com.google.samples.apps.nowinandroid.core.navigation.NavigationState +import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationState import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import dagger.Module import dagger.Provides @@ -30,14 +30,19 @@ import javax.inject.Singleton // 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(): NavigationState = - NavigationState( - startKey = TopLevelDestination.FOR_YOU.key, + fun provideNavigationState(): NiaNavigationState = + NiaNavigationState( + //startKey = TopLevelDestination.FOR_YOU.key, + startKey = object : NiaNavKey { + override val isTopLevel: Boolean + get() = true + } ) // TODO: Remove commented out code @@ -49,11 +54,13 @@ object NavigationStateProvider { // ): 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( @@ -64,3 +71,4 @@ object NavigationStateProvider { } } } +*/ 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 index 0d5a3b5e1..562b3a791 100644 --- 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 @@ -32,7 +32,7 @@ fun NiaNavDisplay( entryProviderBuilders: Set.() -> Unit>, ) { val listDetailStrategy = rememberListDetailSceneStrategy() - val entries = niaNavigator.navigationState.toEntries(entryProviderBuilders) + val entries = niaNavigator.niaNavigationState.toEntries(entryProviderBuilders) NavDisplay( entries = entries, sceneStrategy = listDetailStrategy, diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt index ea5fdb345..13cb66793 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt @@ -18,9 +18,9 @@ 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.core.navigation.NiaNavKey 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 @@ -43,6 +43,7 @@ import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR * @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, @@ -78,3 +79,62 @@ enum class TopLevelDestination( } internal val TopLevelDestinations = TopLevelDestination.entries.associateBy { dest -> dest.key } +*/ + +val FOR_YOU = TopLevelDestination( + selectedIcon = NiaIcons.Upcoming, + unselectedIcon = NiaIcons.UpcomingBorder, + iconTextId = forYouR.string.feature_foryou_api_title, + titleTextId = R.string.app_name, + route = ForYouRoute::class, + key = ForYouRoute, +) + +val BOOKMARKS = TopLevelDestination( + 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, +) + +val INTERESTS = TopLevelDestination( + 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) +) + + +val TOP_LEVEL_ROUTES = mapOf( + ForYouRoute to FOR_YOU, + BookmarksRoute to BOOKMARKS, + InterestsRoute(null) to INTERESTS, +) + + +/** + * 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). + */ +data class TopLevelDestination( + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, + @StringRes val iconTextId: Int, + @StringRes val titleTextId: Int, + val route: KClass<*>, + val key: NavKey, +) 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 50af71980..c0c22e636 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 @@ -37,8 +37,10 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -60,6 +62,10 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.multiplestacks.Navigator import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground @@ -69,11 +75,18 @@ 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.NiaNavKey +import com.google.samples.apps.nowinandroid.core.navigation.simple.toEntries +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.search.api.navigation.navigateToSearch +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry +import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYouEntry +import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry +import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRoute +import com.google.samples.apps.nowinandroid.feature.search.impl.navigation.searchEntry import com.google.samples.apps.nowinandroid.feature.settings.api.SettingsDialog -import com.google.samples.apps.nowinandroid.navigation.NiaNavDisplay -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination +import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry +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.feature.settings.api.R as settingsR @Composable @@ -81,10 +94,10 @@ fun NiaApp( appState: NiaAppState, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), - entryProviderBuilders: Set.() -> Unit>, + //entryProviderBuilders: Set.() -> Unit>, ) { val shouldShowGradientBackground = - appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU + appState.currentTopLevelDestination == FOR_YOU var showSettingsDialog by rememberSaveable { mutableStateOf(false) } NiaBackground(modifier = modifier) { @@ -116,7 +129,7 @@ fun NiaApp( onSettingsDismissed = { showSettingsDialog = false }, onTopAppBarActionClick = { showSettingsDialog = true }, windowAdaptiveInfo = windowAdaptiveInfo, - entryProviderBuilders = entryProviderBuilders, + //entryProviderBuilders = entryProviderBuilders, ) } } @@ -126,7 +139,7 @@ fun NiaApp( @Composable @OptIn( ExperimentalMaterial3Api::class, - ExperimentalComposeUiApi::class, + ExperimentalComposeUiApi::class, ExperimentalMaterial3AdaptiveApi::class, ) internal fun NiaApp( appState: NiaAppState, @@ -135,7 +148,7 @@ internal fun NiaApp( onTopAppBarActionClick: () -> Unit, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), - entryProviderBuilders: Set.() -> Unit>, + //entryProviderBuilders: Set.() -> Unit>, ) { val unreadDestinations by appState.topLevelDestinationsWithUnreadResources .collectAsStateWithLifecycle() @@ -149,14 +162,16 @@ internal fun NiaApp( val snackbarHostState = LocalSnackbarHostState.current + val navigator = remember { Navigator(appState.navigationState) } + NiaNavigationSuiteScaffold( navigationSuiteItems = { - appState.topLevelDestinations.forEach { destination -> + TOP_LEVEL_ROUTES.values.forEach { destination -> val hasUnread = unreadDestinations.contains(destination) val selected = destination.key == currentTopLevelKey item( selected = selected, - onClick = { appState.niaNavigator.navigate(destination.key) }, + onClick = { navigator.navigate(destination.key) }, icon = { Icon( imageVector = destination.unselectedIcon, @@ -227,7 +242,7 @@ internal fun NiaApp( containerColor = Color.Transparent, ), onActionClick = { onTopAppBarActionClick() }, - onNavigationClick = { appState.niaNavigator.navigateToSearch() }, + onNavigationClick = { navigator.navigate(SearchRoute) }, ) } @@ -242,13 +257,20 @@ internal fun NiaApp( ), ) { - // Instantiate the NavigationState here - + val listDetailStrategy = rememberListDetailSceneStrategy() + val entryProvider = entryProvider { + forYouEntry(navigator) + bookmarksEntry(navigator) + interestsEntry(navigator) + topicEntry(navigator) + searchEntry(navigator) + } - NiaNavDisplay( - niaNavigator = appState.niaNavigator, - entryProviderBuilders = entryProviderBuilders, + NavDisplay( + entries = appState.navigationState.toEntries(entryProvider), + sceneStrategy = listDetailStrategy, + onBack = { navigator.goBack() }, ) } 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 c6595f0ac..0c459b058 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 @@ -24,10 +24,14 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc 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.NiaNavigator +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.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.TopLevelDestination -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKMARKS -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestinations +import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_ROUTES +//import com.google.samples.apps.nowinandroid.navigation.TopLevelDestinations import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -41,19 +45,24 @@ fun rememberNiaAppState( networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, - niaNavigator: NiaNavigator, + //niaNavigator: NiaNavigator, coroutineScope: CoroutineScope = rememberCoroutineScope(), ): NiaAppState { - NavigationTrackingSideEffect(niaNavigator) + //NavigationTrackingSideEffect(niaNavigator) + + val navigationState = rememberNavigationState(ForYouRoute, TOP_LEVEL_ROUTES.keys) + return remember( - niaNavigator, + //niaNavigator, + navigationState, coroutineScope, networkMonitor, userNewsResourceRepository, timeZoneMonitor, ) { NiaAppState( - niaNavigator = niaNavigator, + //niaNavigator = niaNavigator, + navigationState = navigationState, coroutineScope = coroutineScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, @@ -64,14 +73,20 @@ fun rememberNiaAppState( @Stable class NiaAppState( - val niaNavigator: NiaNavigator, + //val niaNavigator: NiaNavigator, + val navigationState: NavigationState, coroutineScope: CoroutineScope, networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, ) { +/* + val currentTopLevelDestination: TopLevelDestination? + @Composable get() = TOP_LEVEL_ROUTES[niaNavigator.navigationState.currentTopLevelKey] +*/ + // TODO: It seems unnecessary to expose this as a TopLevelDestination rather than just a key val currentTopLevelDestination: TopLevelDestination? - @Composable get() = TopLevelDestinations[niaNavigator.navigationState.currentTopLevelKey] + @Composable get() = TOP_LEVEL_ROUTES[navigationState.topLevelRoute] val isOffline = networkMonitor.isOnline .map(Boolean::not) @@ -81,12 +96,6 @@ class NiaAppState( initialValue = false, ) - /** - * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the - * route. - */ - val topLevelDestinations: List = TopLevelDestination.entries - /** * The top level destinations that have unread news resources. */ @@ -115,6 +124,8 @@ class NiaAppState( /** * Stores information about navigation events to be used with JankStats */ + +// TODO: This shouldn't be commented out @Composable private fun NavigationTrackingSideEffect(niaNavigator: NiaNavigator) { // TrackDisposableJank(niaNavigator) { metricsHolder -> diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index 90880d5f2..31b2ee5df 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -25,6 +25,7 @@ import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor 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.TOP_LEVEL_ROUTES import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import kotlinx.coroutines.flow.collect @@ -100,10 +101,10 @@ class NiaAppStateTest { ) } - assertEquals(3, state.topLevelDestinations.size) - assertTrue(state.topLevelDestinations[0].name.contains("for_you", true)) - assertTrue(state.topLevelDestinations[1].name.contains("bookmarks", true)) - assertTrue(state.topLevelDestinations[2].name.contains("interests", true)) + assertEquals(3, TOP_LEVEL_ROUTES.size) + assertTrue(TOP_LEVEL_ROUTES[0].name.contains("for_you", true)) + assertTrue(TOP_LEVEL_ROUTES[1].name.contains("bookmarks", true)) + assertTrue(TOP_LEVEL_ROUTES[2].name.contains("interests", true)) } @Test 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 index 1704a6151..d0c1006ac 100644 --- 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 @@ -48,7 +48,7 @@ class NiaBackStackViewModelTest { private fun createViewModel() = NiaBackStackViewModel( savedStateHandle = SavedStateHandle(), - navigationState = NavigationState(TestStartKey), + niaNavigationState = NiaNavigationState(TestStartKey), serializersModules = serializersModules, ) @@ -67,7 +67,7 @@ class NiaBackStackViewModelTest { fun testNonTopLevelKeySaved() { val viewModel = createViewModel() rule.setContent { - val navigator = remember { NiaNavigator(viewModel.navigationState) } + val navigator = remember { NiaNavigator(viewModel.niaNavigationState) } navigator.navigate(TestKeyFirst) } assertThat(viewModel.backStackMap.size).isEqualTo(1) @@ -80,7 +80,7 @@ class NiaBackStackViewModelTest { fun testTopLevelKeySaved() { val viewModel = createViewModel() rule.setContent { - val navigator = remember { NiaNavigator(viewModel.navigationState) } + val navigator = remember { NiaNavigator(viewModel.niaNavigationState) } navigator.navigate(TestKeyFirst) navigator.navigate(TestTopLevelKeyFirst) @@ -101,7 +101,7 @@ class NiaBackStackViewModelTest { fun testMultiStacksSaved() { val viewModel = createViewModel() rule.setContent { - val navigator = remember { NiaNavigator(viewModel.navigationState) } + val navigator = remember { NiaNavigator(viewModel.niaNavigationState) } navigator.navigate(TestKeyFirst) navigator.navigate(TestTopLevelKeyFirst) navigator.navigate(TestKeySecond) @@ -122,7 +122,7 @@ class NiaBackStackViewModelTest { fun testPopSaved() { val viewModel = createViewModel() rule.setContent { - val navigator = remember { NiaNavigator(viewModel.navigationState) } + val navigator = remember { NiaNavigator(viewModel.niaNavigationState) } navigator.navigate(TestKeyFirst) @@ -144,14 +144,14 @@ class NiaBackStackViewModelTest { fun testRestore() { lateinit var scenario: ViewModelScenario lateinit var navigator: NiaNavigator - lateinit var navigatorState: NavigationState + lateinit var navigatorState: NiaNavigationState rule.setContent { - navigatorState = remember { NavigationState(TestStartKey) } + navigatorState = remember { NiaNavigationState(TestStartKey) } navigator = remember { NiaNavigator(navigatorState) } scenario = viewModelScenario { NiaBackStackViewModel( savedStateHandle = createSavedStateHandle(), - navigationState = navigatorState, + niaNavigationState = navigatorState, serializersModules = serializersModules, ) } @@ -181,14 +181,14 @@ class NiaBackStackViewModelTest { fun testRestoreMultiStacks() { lateinit var scenario: ViewModelScenario lateinit var navigator: NiaNavigator - lateinit var navigatorState: NavigationState + lateinit var navigatorState: NiaNavigationState rule.setContent { - navigatorState = remember { NavigationState(TestStartKey) } + navigatorState = remember { NiaNavigationState(TestStartKey) } navigator = remember { NiaNavigator(navigatorState) } scenario = viewModelScenario { NiaBackStackViewModel( savedStateHandle = createSavedStateHandle(), - navigationState = navigatorState, + niaNavigationState = navigatorState, serializersModules = serializersModules, ) } 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 index 3c4b721c8..323c90375 100644 --- 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 @@ -39,10 +39,11 @@ import javax.inject.Inject * 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 navigationState: NavigationState, + val niaNavigationState: NiaNavigationState, serializersModules: SerializersModule, ) : ViewModel() { @@ -71,7 +72,7 @@ class NiaBackStackViewModel @Inject constructor( if (backStackMap.isNotEmpty()) { // Restore backstack from saved state handle if not empty @Suppress("UNCHECKED_CAST") - navigationState.restore( + niaNavigationState.restore( activeTopLeveLKeys, backStackMap as LinkedHashMap>, ) @@ -80,9 +81,10 @@ class NiaBackStackViewModel @Inject constructor( // Start observing changes to the backStack and save backStack whenever it updates viewModelScope.launch { snapshotFlow { - activeTopLeveLKeys = navigationState.activeTopLeveLKeys.toList() - backStackMap = navigationState.backStacks + 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 index 27f7bda13..4e4659678 100644 --- 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 @@ -35,7 +35,7 @@ import javax.inject.Inject import kotlin.collections.plus // TODO: Consider changing this to `NiaNavigationState` -class NavigationState( +class NiaNavigationState( internal val startKey: NiaNavKey, ) { internal var backStacks: MutableMap> = @@ -81,14 +81,14 @@ class NavigationState( * TODO: Document this */ class NiaNavigator @Inject constructor( - val navigationState: NavigationState, + 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() - navigationState.apply { + niaNavigationState.apply { currentActiveSubStacks.addAll(activeTopLeveLKeys) when { // top level singleTop -> clear substack @@ -126,7 +126,7 @@ class NiaNavigator @Inject constructor( } fun pop() { - navigationState.apply { + 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 @@ -149,7 +149,7 @@ interface NiaNavKey { * Convert the navigation state to `NavEntry`s that can be displayed by a `NavDisplay` */ @Composable -fun NavigationState.toEntries( +fun NiaNavigationState.toEntries( // TODO: Might be better to pass this in fully constructed entryProviderBuilders: Set.() -> Unit>, ): List> = 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/simple/NavigationState.kt new file mode 100644 index 000000000..0e84aefef --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/NavigationState.kt @@ -0,0 +1,108 @@ +/* + * 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.compose.runtime.Composable +import androidx.compose.runtime.MutableState +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 +import androidx.navigation3.runtime.NavEntry +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 +): NavigationState { + + val topLevelRoute = rememberSerializable( + startRoute, topLevelRoutes, + serializer = MutableStateSerializer(NavKeySerializer()) + ) { + mutableStateOf(startRoute) + } + + val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } + + return remember(startRoute, topLevelRoutes) { + NavigationState( + startRoute = startRoute, + topLevelRoute = topLevelRoute, + backStacks = backStacks + ) + } +} + +/** + * 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 + */ +class NavigationState( + val startRoute: NavKey, + topLevelRoute: MutableState, + val backStacks: Map> +) { + var topLevelRoute: NavKey by topLevelRoute + val stacksInUse: List + get() = if (topLevelRoute == startRoute) { + listOf(startRoute) + } else { + listOf(startRoute, topLevelRoute) + } + +} + +/** + * Convert NavigationState into NavEntries. + */ +@Composable +fun NavigationState.toEntries( + entryProvider: (NavKey) -> NavEntry +): SnapshotStateList> { + + val decoratedEntries = backStacks.mapValues { (_, stack) -> + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + ) + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = decorators, + entryProvider = entryProvider + ) + } + + return stacksInUse + .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/simple/Navigator.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/Navigator.kt new file mode 100644 index 000000000..e2ca6bfa7 --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/simple/Navigator.kt @@ -0,0 +1,47 @@ +/* + * 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.example.nav3recipes.multiplestacks + +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.simple.NavigationState + +/** + * 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/NiaNavigatorStateTest.kt b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorStateTest.kt index 2bb4db7b8..4b2342122 100644 --- 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 @@ -24,49 +24,49 @@ import kotlin.test.assertFailsWith class NiaNavigatorStateTest { - private lateinit var navigationState: NavigationState + private lateinit var niaNavigationState: NiaNavigationState private lateinit var niaNavigator: NiaNavigator @Before fun setup() { - navigationState = NavigationState(TestStartKey) - niaNavigator = NiaNavigator(navigationState) + niaNavigationState = NiaNavigationState(TestStartKey) + niaNavigator = NiaNavigator(niaNavigationState) } @Test fun testStartKey() { - assertThat(navigationState.currentKey).isEqualTo(TestStartKey) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test fun testNavigate() { niaNavigator.navigate(TestKeyFirst) - assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test fun testNavigateTopLevel() { niaNavigator.navigate(TestTopLevelKey) - assertThat(navigationState.currentKey).isEqualTo(TestTopLevelKey) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) } @Test fun testNavigateSingleTop() { niaNavigator.navigate(TestKeyFirst) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, ).inOrder() niaNavigator.navigate(TestKeyFirst) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, ).inOrder() @@ -77,7 +77,7 @@ class NiaNavigatorStateTest { niaNavigator.navigate(TestTopLevelKey) niaNavigator.navigate(TestKeyFirst) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestTopLevelKey, TestKeyFirst, @@ -85,7 +85,7 @@ class NiaNavigatorStateTest { niaNavigator.navigate(TestTopLevelKey) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestTopLevelKey, ).inOrder() @@ -95,13 +95,13 @@ class NiaNavigatorStateTest { fun testSubStack() { niaNavigator.navigate(TestKeyFirst) - assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) niaNavigator.navigate(TestKeySecond) - assertThat(navigationState.currentKey).isEqualTo(TestKeySecond) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeySecond) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test @@ -109,33 +109,33 @@ class NiaNavigatorStateTest { // add to start stack niaNavigator.navigate(TestKeyFirst) - assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) // navigate to new top level niaNavigator.navigate(TestTopLevelKey) - assertThat(navigationState.currentKey).isEqualTo(TestTopLevelKey) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) // add to new stack niaNavigator.navigate(TestKeySecond) - assertThat(navigationState.currentKey).isEqualTo(TestKeySecond) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeySecond) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) // go back to start stack niaNavigator.navigate(TestStartKey) - assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test fun testRestore() { - assertThat(navigationState.currentBackStack).containsExactly(TestStartKey) + assertThat(niaNavigationState.currentBackStack).containsExactly(TestStartKey) - navigationState.restore( + niaNavigationState.restore( listOf(TestStartKey, TestTopLevelKey), linkedMapOf( TestStartKey to mutableStateListOf(TestStartKey, TestKeyFirst), @@ -143,15 +143,15 @@ class NiaNavigatorStateTest { ), ) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, TestTopLevelKey, TestKeySecond, ).inOrder() - assertThat(navigationState.currentKey).isEqualTo(TestKeySecond) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeySecond) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) } @Test @@ -159,7 +159,7 @@ class NiaNavigatorStateTest { niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeySecond) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, TestKeySecond, @@ -167,13 +167,13 @@ class NiaNavigatorStateTest { niaNavigator.pop() - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, ).inOrder() - assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test @@ -181,25 +181,25 @@ class NiaNavigatorStateTest { niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestTopLevelKey) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, TestTopLevelKey, ).inOrder() - assertThat(navigationState.currentKey).isEqualTo(TestTopLevelKey) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestTopLevelKey) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey) // remove TopLevel niaNavigator.pop() - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, ).inOrder() - assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test @@ -207,7 +207,7 @@ class NiaNavigatorStateTest { niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeySecond) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestKeyFirst, TestKeySecond, @@ -216,12 +216,12 @@ class NiaNavigatorStateTest { niaNavigator.pop() niaNavigator.pop() - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, ).inOrder() - assertThat(navigationState.currentKey).isEqualTo(TestStartKey) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test @@ -238,7 +238,7 @@ class NiaNavigatorStateTest { niaNavigator.navigate(testTopLevelKeyTwo) niaNavigator.navigate(TestKeySecond) - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, TestTopLevelKey, TestKeyFirst, @@ -250,12 +250,12 @@ class NiaNavigatorStateTest { niaNavigator.pop() } - assertThat(navigationState.currentBackStack).containsExactly( + assertThat(niaNavigationState.currentBackStack).containsExactly( TestStartKey, ).inOrder() - assertThat(navigationState.currentKey).isEqualTo(TestStartKey) - assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentKey).isEqualTo(TestStartKey) + assertThat(niaNavigationState.currentTopLevelKey).isEqualTo(TestStartKey) } @Test diff --git a/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksRoute.kt b/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksRoute.kt index e3955176b..c96b49392 100644 --- a/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksRoute.kt +++ b/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksRoute.kt @@ -16,11 +16,15 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation +import androidx.navigation3.runtime.NavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import kotlinx.serialization.Serializable -@Serializable +/*@Serializable object BookmarksRoute : NiaNavKey { override val isTopLevel: Boolean get() = true -} +}*/ + +@Serializable +object BookmarksRoute : NavKey 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 aed257d84..5b9468b2d 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 @@ -21,11 +21,14 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.runtime.compositionLocalOf import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.multiplestacks.Navigator import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator 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.navigateToTopic +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 @@ -39,21 +42,23 @@ object BookmarksEntryProvider { @Provides @IntoSet fun provideBookmarksEntryProviderBuilder( - navigator: NiaNavigator, - ): EntryProviderScope.() -> Unit = { - entry { - val snackbarHostState = LocalSnackbarHostState.current - BookmarksScreen( - onTopicClick = navigator::navigateToTopic, - onShowSnackbar = { message, action -> - snackbarHostState.showSnackbar( - message = message, - actionLabel = action, - duration = Short, - ) == ActionPerformed - }, - ) - } + navigator: Navigator, + ): EntryProviderScope.() -> Unit = { bookmarksEntry(navigator) } +} + +fun EntryProviderScope.bookmarksEntry(navigator: Navigator) { + entry { + val snackbarHostState = LocalSnackbarHostState.current + BookmarksScreen( + onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + onShowSnackbar = { message, action -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = Short, + ) == ActionPerformed + }, + ) } } 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 index b26bb646c..36390833b 100644 --- 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 @@ -29,6 +29,7 @@ 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 { @@ -38,3 +39,4 @@ object BookmarksSerializerModule { subclass(BookmarksRoute::class, BookmarksRoute.serializer()) } } +*/ diff --git a/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouRoute.kt b/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouRoute.kt index 2467649a4..396a8dfc0 100644 --- a/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouRoute.kt +++ b/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouRoute.kt @@ -16,11 +16,17 @@ package com.google.samples.apps.nowinandroid.feature.foryou.api.navigation +import androidx.navigation3.runtime.NavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import kotlinx.serialization.Serializable +/* @Serializable object ForYouRoute : NiaNavKey { // route to ForYou screen override val isTopLevel: Boolean get() = true } +*/ + +@Serializable +object ForYouRoute : NavKey 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 f4fd9ab81..80f48c55e 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 @@ -17,11 +17,14 @@ package com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.multiplestacks.Navigator import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator 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.navigateToTopic +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 @@ -38,14 +41,14 @@ object ForYouEntryProvider { @Provides @IntoSet fun provideForYouEntryProviderBuilder( - navigator: NiaNavigator, - ): EntryProviderScope.() -> Unit = forYouEntry(navigator) + navigator: Navigator, + ): EntryProviderScope.() -> Unit = { forYouEntry(navigator) } } -fun forYouEntry(navigator: NiaNavigator): EntryProviderScope.() -> Unit = { +fun EntryProviderScope.forYouEntry(navigator: Navigator) { entry { ForYouScreen( - onTopicClick = navigator::navigateToTopic, + onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, ) } } 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 index 15c010fb3..4d6cbaf5d 100644 --- 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 @@ -29,6 +29,7 @@ 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 { @@ -38,3 +39,4 @@ object ForYouRouteSerializerModule { subclass(ForYouRoute::class, ForYouRoute.serializer()) } } +*/ diff --git a/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsRoute.kt b/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsRoute.kt index ce48a1df6..36bffbc08 100644 --- a/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsRoute.kt +++ b/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsRoute.kt @@ -16,9 +16,11 @@ package com.google.samples.apps.nowinandroid.feature.interests.api.navigation +import androidx.navigation3.runtime.NavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import kotlinx.serialization.Serializable +/* @Serializable data class InterestsRoute( // The ID of the topic which will be initially selected at this destination @@ -27,3 +29,11 @@ data class InterestsRoute( override val isTopLevel: Boolean get() = true } +*/ + + +@Serializable +data class InterestsRoute( + // The ID of the topic which will be initially selected at this destination + val initialTopicId: String? = null, +) : NavKey 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 60eb4c4ce..40f8b4c69 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 @@ -20,13 +20,16 @@ import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi 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.example.nav3recipes.multiplestacks.Navigator import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator 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.navigateToTopic +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 @@ -41,25 +44,29 @@ object InterestsEntryProvider { @Provides @IntoSet fun provideInterestsEntryProviderBuilder( - navigator: NiaNavigator, - ): EntryProviderScope.() -> Unit = { - entry( - metadata = ListDetailSceneStrategy.listPane { - InterestsDetailPlaceholder() - }, - ) { key -> - val viewModel = hiltViewModel { - it.create(key) - } - InterestsScreen( - // TODO: This event should be provided by the ViewModel - onTopicClick = navigator::navigateToTopic, + navigator: Navigator, + ): EntryProviderScope.() -> Unit = { interestsEntry(navigator) } +} - // TODO: This should be dynamically calculated based on the rendering scene - // See https://github.com/android/nav3-recipes/commit/488f4811791ca3ed7192f4fe3c86e7371b32ebdc#diff-374e02026cdd2f68057dd940f203dc4ba7319930b33e9555c61af7e072211cabR89 - shouldHighlightSelectedTopic = false, - viewModel = viewModel, - ) +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun EntryProviderScope.interestsEntry(navigator: Navigator) { + entry( + metadata = ListDetailSceneStrategy.listPane { + InterestsDetailPlaceholder() + }, + ) { key -> + val viewModel = hiltViewModel { + 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 should be dynamically calculated based on the rendering scene + // See https://github.com/android/nav3-recipes/commit/488f4811791ca3ed7192f4fe3c86e7371b32ebdc#diff-374e02026cdd2f68057dd940f203dc4ba7319930b33e9555c61af7e072211cabR89 + shouldHighlightSelectedTopic = false, + viewModel = viewModel, + ) } } 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 index cdc1234b8..03e9b0437 100644 --- 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 @@ -29,6 +29,7 @@ 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 { @@ -38,3 +39,4 @@ object InterestsSerializerModule { 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 f2e135e0b..2606a748f 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 @@ -16,10 +16,12 @@ 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 @@ -29,3 +31,7 @@ object SearchRoute : NiaNavKey { 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 f62d3cf43..d8174c8d2 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 @@ -17,12 +17,15 @@ package com.google.samples.apps.nowinandroid.feature.search.impl.navigation import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.multiplestacks.Navigator import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator 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.navigateToTopic +import com.google.samples.apps.nowinandroid.feature.search.impl.navigation.searchEntry +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -36,14 +39,16 @@ object SearchEntryProvider { @Provides @IntoSet fun provideSearchEntryProviderBuilder( - navigator: NiaNavigator, - ): EntryProviderScope.() -> Unit = { - entry { key -> - SearchScreen( - onBackClick = navigator::pop, - onInterestsClick = { navigator.navigate(InterestsRoute()) }, - onTopicClick = navigator::navigateToTopic, - ) - } + navigator: Navigator, + ): EntryProviderScope.() -> Unit = { searchEntry(navigator) } +} + +fun EntryProviderScope.searchEntry(navigator: Navigator) { + entry { key -> + SearchScreen( + onBackClick = { navigator.goBack() }, + onInterestsClick = { navigator.navigate(InterestsRoute()) }, + onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + ) } } 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 index eca89cb40..6bf6a0e97 100644 --- 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 @@ -29,7 +29,7 @@ import kotlinx.serialization.modules.PolymorphicModuleBuilder * Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer * */ -@Module +/*@Module @InstallIn(SingletonComponent::class) object SearchSerializerModule { @Provides @@ -37,4 +37,4 @@ object SearchSerializerModule { 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 5289c99ff..a331aaf27 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 @@ -16,10 +16,12 @@ 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 kotlinx.serialization.Serializable +/* @Serializable data class TopicRoute(val id: String) : NiaNavKey { override val isTopLevel: Boolean @@ -31,3 +33,7 @@ fun NiaNavigator.navigateToTopic( ) { 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 index 6286efdbd..4bb36e2f0 100644 --- 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 @@ -28,7 +28,7 @@ import kotlinx.serialization.modules.PolymorphicModuleBuilder * Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer * */ -@Module +/*@Module @InstallIn(SingletonComponent::class) object TopicSerializerModule { @Provides @@ -36,4 +36,4 @@ object TopicSerializerModule { 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 6cf48ad14..ecd928d75 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 @@ -20,10 +20,12 @@ import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi 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.example.nav3recipes.multiplestacks.Navigator import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator 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 @@ -41,22 +43,25 @@ object TopicEntryProvider { @Provides @IntoSet fun provideTopicEntryProviderBuilder( - navigator: NiaNavigator, - ): EntryProviderScope.() -> Unit = { - entry( - metadata = ListDetailSceneStrategy.detailPane(), - ) { key -> - val id = key.id - TopicScreen( - showBackButton = true, - onBackClick = navigator::pop, - onTopicClick = navigator::navigateToTopic, - viewModel = hiltViewModel( - key = id, - ) { factory -> - factory.create(id) - }, - ) - } + navigator: Navigator, + ): EntryProviderScope.() -> Unit = { topicEntry(navigator) } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun EntryProviderScope.topicEntry(navigator: Navigator) { + entry( + metadata = ListDetailSceneStrategy.detailPane(), + ) { key -> + val id = key.id + TopicScreen( + showBackButton = true, + onBackClick = { navigator.goBack() }, + onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) }, + viewModel = hiltViewModel( + key = id, + ) { factory -> + factory.create(id) + }, + ) } }