WIP remove DI

pull/2003/head
Don Turner 2 months ago
parent adcc3871be
commit c9bba49957

@ -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<NiaNavKey>.() -> Unit>
@Inject
lateinit var niaNavigator: NiaNavigator
/*@Inject
lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderScope<NavKey>.() -> 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
)
}
}

@ -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 {
}
}
}
*/

@ -32,7 +32,7 @@ fun NiaNavDisplay(
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
) {
val listDetailStrategy = rememberListDetailSceneStrategy<NiaNavKey>()
val entries = niaNavigator.navigationState.toEntries(entryProviderBuilders)
val entries = niaNavigator.niaNavigationState.toEntries(entryProviderBuilders)
NavDisplay(
entries = entries,
sceneStrategy = listDetailStrategy,

@ -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<NavKey, TopLevelDestination>(
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,
)

@ -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<EntryProviderScope<NiaNavKey>.() -> Unit>,
//entryProviderBuilders: Set<EntryProviderScope<NavKey>.() -> 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<EntryProviderScope<NiaNavKey>.() -> Unit>,
//entryProviderBuilders: Set<EntryProviderScope<NavKey>.() -> 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<NavKey>()
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() },
)
}

@ -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> = 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 ->

@ -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

@ -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<NiaBackStackViewModel>
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<NiaBackStackViewModel>
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,
)
}

@ -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<NiaNavKey, SnapshotStateList<NiaNavKey>>,
)
@ -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()
}
}
}
*/

@ -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<NiaNavKey, SnapshotStateList<NiaNavKey>> =
@ -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<NiaNavKey>()
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<EntryProviderScope<NiaNavKey>.() -> Unit>,
): List<NavEntry<NiaNavKey>> =

@ -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<NavKey>
): 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<NavKey>,
val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
var topLevelRoute: NavKey by topLevelRoute
val stacksInUse: List<NavKey>
get() = if (topLevelRoute == startRoute) {
listOf(startRoute)
} else {
listOf(startRoute, topLevelRoute)
}
}
/**
* Convert NavigationState into NavEntries.
*/
@Composable
fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {
val decoratedEntries = backStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider
)
}
return stacksInUse
.flatMap { decoratedEntries[it] ?: emptyList() }
.toMutableStateList()
}

@ -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()
}
}
}

@ -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

@ -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

@ -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<NiaNavKey>.() -> Unit = {
entry<BookmarksRoute> {
val snackbarHostState = LocalSnackbarHostState.current
BookmarksScreen(
onTopicClick = navigator::navigateToTopic,
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,
actionLabel = action,
duration = Short,
) == ActionPerformed
},
)
}
navigator: Navigator,
): EntryProviderScope<NavKey>.() -> Unit = { bookmarksEntry(navigator) }
}
fun EntryProviderScope<NavKey>.bookmarksEntry(navigator: Navigator) {
entry<BookmarksRoute> {
val snackbarHostState = LocalSnackbarHostState.current
BookmarksScreen(
onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) },
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,
actionLabel = action,
duration = Short,
) == ActionPerformed
},
)
}
}

@ -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())
}
}
*/

@ -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

@ -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<NiaNavKey>.() -> Unit = forYouEntry(navigator)
navigator: Navigator,
): EntryProviderScope<NavKey>.() -> Unit = { forYouEntry(navigator) }
}
fun forYouEntry(navigator: NiaNavigator): EntryProviderScope<NiaNavKey>.() -> Unit = {
fun EntryProviderScope<NavKey>.forYouEntry(navigator: Navigator) {
entry<ForYouRoute> {
ForYouScreen(
onTopicClick = navigator::navigateToTopic,
onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) },
)
}
}

@ -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())
}
}
*/

@ -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

@ -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<NiaNavKey>.() -> Unit = {
entry<InterestsRoute>(
metadata = ListDetailSceneStrategy.listPane {
InterestsDetailPlaceholder()
},
) { key ->
val viewModel = hiltViewModel<InterestsViewModel, InterestsViewModel.Factory> {
it.create(key)
}
InterestsScreen(
// TODO: This event should be provided by the ViewModel
onTopicClick = navigator::navigateToTopic,
navigator: Navigator,
): EntryProviderScope<NavKey>.() -> 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<NavKey>.interestsEntry(navigator: Navigator) {
entry<InterestsRoute>(
metadata = ListDetailSceneStrategy.listPane {
InterestsDetailPlaceholder()
},
) { key ->
val viewModel = hiltViewModel<InterestsViewModel, InterestsViewModel.Factory> {
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,
)
}
}

@ -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())
}
}
*/

@ -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

@ -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<NiaNavKey>.() -> Unit = {
entry<SearchRoute> { key ->
SearchScreen(
onBackClick = navigator::pop,
onInterestsClick = { navigator.navigate(InterestsRoute()) },
onTopicClick = navigator::navigateToTopic,
)
}
navigator: Navigator,
): EntryProviderScope<NavKey>.() -> Unit = { searchEntry(navigator) }
}
fun EntryProviderScope<NavKey>.searchEntry(navigator: Navigator) {
entry<SearchRoute> { key ->
SearchScreen(
onBackClick = { navigator.goBack() },
onInterestsClick = { navigator.navigate(InterestsRoute()) },
onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) },
)
}
}

@ -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())
}
}
}*/

@ -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

@ -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())
}
}
}*/

@ -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<NiaNavKey>.() -> Unit = {
entry<TopicRoute>(
metadata = ListDetailSceneStrategy.detailPane(),
) { key ->
val id = key.id
TopicScreen(
showBackButton = true,
onBackClick = navigator::pop,
onTopicClick = navigator::navigateToTopic,
viewModel = hiltViewModel<TopicViewModel, Factory>(
key = id,
) { factory ->
factory.create(id)
},
)
}
navigator: Navigator,
): EntryProviderScope<NavKey>.() -> Unit = { topicEntry(navigator) }
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
fun EntryProviderScope<NavKey>.topicEntry(navigator: Navigator) {
entry<TopicRoute>(
metadata = ListDetailSceneStrategy.detailPane(),
) { key ->
val id = key.id
TopicScreen(
showBackButton = true,
onBackClick = { navigator.goBack() },
onTopicClick = { topicId -> navigator.navigate(TopicRoute(topicId)) },
viewModel = hiltViewModel<TopicViewModel, Factory>(
key = id,
) { factory ->
factory.create(id)
},
)
}
}

Loading…
Cancel
Save