Refactor navigation routes to NavKeys

This commit refactors the navigation implementation by renaming all `...Route` classes to `...NavKey`. This change provides more descriptive and consistent naming for navigation keys across the codebase.

Key changes include:
*   Renamed `BookmarksRoute` to `BookmarksNavKey`
*   Renamed `ForYouRoute` to `ForYouNavKey`
*   Renamed `InterestsRoute` to `InterestsNavKey`
*   Renamed `TopicRoute` to `TopicNavKey`
*   Renamed `SearchRoute` to `SearchNavKey`
*   Updated all associated feature modules, tests, and UI components to use the new `NavKey` names.
*   Removed obsolete test utilities and mock providers related to the old navigation setup.
*   Deleted outdated dependency graph images and their corresponding `README.md` files from feature modules.
pull/2003/head
Don Turner 2 weeks ago
parent bd484ebb3c
commit b9a80fc038

@ -75,7 +75,6 @@ class MainActivity : ComponentActivity() {
private val viewModel: MainActivityViewModel by viewModels() private val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -134,7 +133,6 @@ class MainActivity : ComponentActivity() {
splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() } splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() }
setContent { setContent {
val appState = rememberNiaAppState( val appState = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,

@ -18,6 +18,14 @@ package com.google.samples.apps.nowinandroid.navigation
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR
import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR
/** /**
* Type for the top level navigation items in the application. Contains UI information about the * Type for the top level navigation items in the application. Contains UI information about the
@ -36,3 +44,30 @@ data class TopLevelNavItem(
@StringRes val iconTextId: Int, @StringRes val iconTextId: Int,
@StringRes val titleTextId: Int, @StringRes val titleTextId: Int,
) )
val FOR_YOU = TopLevelNavItem(
selectedIcon = NiaIcons.Upcoming,
unselectedIcon = NiaIcons.UpcomingBorder,
iconTextId = forYouR.string.feature_foryou_api_title,
titleTextId = R.string.app_name,
)
val BOOKMARKS = TopLevelNavItem(
selectedIcon = NiaIcons.Bookmarks,
unselectedIcon = NiaIcons.BookmarksBorder,
iconTextId = bookmarksR.string.feature_bookmarks_api_title,
titleTextId = bookmarksR.string.feature_bookmarks_api_title,
)
val INTERESTS = TopLevelNavItem(
selectedIcon = NiaIcons.Grid3x3,
unselectedIcon = NiaIcons.Grid3x3,
iconTextId = searchR.string.feature_search_api_interests,
titleTextId = searchR.string.feature_search_api_interests,
)
val TOP_LEVEL_NAV_ITEMS = mapOf(
ForYouNavKey to FOR_YOU,
BookmarksNavKey to BOOKMARKS,
InterestsNavKey(null) to INTERESTS,
)

@ -1,53 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.navigation
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR
import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR
val FOR_YOU = TopLevelNavItem(
selectedIcon = NiaIcons.Upcoming,
unselectedIcon = NiaIcons.UpcomingBorder,
iconTextId = forYouR.string.feature_foryou_api_title,
titleTextId = R.string.app_name,
)
val BOOKMARKS = TopLevelNavItem(
selectedIcon = NiaIcons.Bookmarks,
unselectedIcon = NiaIcons.BookmarksBorder,
iconTextId = bookmarksR.string.feature_bookmarks_api_title,
titleTextId = bookmarksR.string.feature_bookmarks_api_title,
)
val INTERESTS = TopLevelNavItem(
selectedIcon = NiaIcons.Grid3x3,
unselectedIcon = NiaIcons.Grid3x3,
iconTextId = searchR.string.feature_search_api_interests,
titleTextId = searchR.string.feature_search_api_interests,
)
val TOP_LEVEL_NAV_ITEMS = mapOf(
ForYouRoute to FOR_YOU,
BookmarksRoute to BOOKMARKS,
InterestsRoute(null) to INTERESTS,
)

@ -76,13 +76,13 @@ import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.core.navigation.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.bookmarks.impl.navigation.LocalSnackbarHostState
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey
import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYouEntry 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.interests.impl.navigation.interestsEntry
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRoute import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey
import com.google.samples.apps.nowinandroid.feature.search.impl.navigation.searchEntry 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.feature.settings.api.SettingsDialog
import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry 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_NAV_ITEMS import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS
import com.google.samples.apps.nowinandroid.feature.settings.api.R as settingsR import com.google.samples.apps.nowinandroid.feature.settings.api.R as settingsR
@ -92,7 +92,7 @@ fun NiaApp(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) { ) {
val shouldShowGradientBackground = appState.currentTopLevelNavItem == FOR_YOU val shouldShowGradientBackground = appState.navigationState.currentTopLevelKey == ForYouNavKey
var showSettingsDialog by rememberSaveable { mutableStateOf(false) } var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
NiaBackground(modifier = modifier) { NiaBackground(modifier = modifier) {
@ -135,7 +135,8 @@ fun NiaApp(
@Composable @Composable
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalComposeUiApi::class, ExperimentalMaterial3AdaptiveApi::class, ExperimentalComposeUiApi::class,
ExperimentalMaterial3AdaptiveApi::class,
) )
internal fun NiaApp( internal fun NiaApp(
appState: NiaAppState, appState: NiaAppState,
@ -145,7 +146,7 @@ internal fun NiaApp(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) { ) {
val unreadRoutes by appState.topLevelRoutesWithUnreadResources val unreadNavKeys by appState.topLevelNavKeysWithUnreadResources
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
if (showSettingsDialog) { if (showSettingsDialog) {
@ -161,7 +162,7 @@ internal fun NiaApp(
NiaNavigationSuiteScaffold( NiaNavigationSuiteScaffold(
navigationSuiteItems = { navigationSuiteItems = {
TOP_LEVEL_NAV_ITEMS.forEach { (navKey, navItem) -> TOP_LEVEL_NAV_ITEMS.forEach { (navKey, navItem) ->
val hasUnread = unreadRoutes.contains(navKey) val hasUnread = unreadNavKeys.contains(navKey)
val selected = navKey == appState.navigationState.currentTopLevelKey val selected = navKey == appState.navigationState.currentTopLevelKey
item( item(
selected = selected, selected = selected,
@ -239,7 +240,7 @@ internal fun NiaApp(
containerColor = Color.Transparent, containerColor = Color.Transparent,
), ),
onActionClick = { onTopAppBarActionClick() }, onActionClick = { onTopAppBarActionClick() },
onNavigationClick = { navigator.navigate(SearchRoute) }, onNavigationClick = { navigator.navigate(SearchNavKey) },
) )
} }
@ -253,7 +254,6 @@ internal fun NiaApp(
}, },
), ),
) { ) {
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()
val entryProvider = entryProvider { val entryProvider = entryProvider {

@ -26,10 +26,10 @@ 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.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.navigation.NavigationState import com.google.samples.apps.nowinandroid.core.navigation.NavigationState
import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey
import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS
import com.google.samples.apps.nowinandroid.navigation.TopLevelNavItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -45,8 +45,7 @@ fun rememberNiaAppState(
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
coroutineScope: CoroutineScope = rememberCoroutineScope(), coroutineScope: CoroutineScope = rememberCoroutineScope(),
): NiaAppState { ): NiaAppState {
val navigationState = rememberNavigationState(ForYouNavKey, TOP_LEVEL_NAV_ITEMS.keys)
val navigationState = rememberNavigationState(ForYouRoute, TOP_LEVEL_NAV_ITEMS.keys)
NavigationTrackingSideEffect(navigationState) NavigationTrackingSideEffect(navigationState)
@ -75,10 +74,6 @@ class NiaAppState(
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
) { ) {
// TODO: I think this should return null if the current route is not a topLevelRoute
val currentTopLevelNavItem: TopLevelNavItem?
@Composable get() = TOP_LEVEL_NAV_ITEMS[navigationState.currentTopLevelKey]
val isOffline = networkMonitor.isOnline val isOffline = networkMonitor.isOnline
.map(Boolean::not) .map(Boolean::not)
.stateIn( .stateIn(
@ -88,14 +83,14 @@ class NiaAppState(
) )
/** /**
* The top level routes that have unread news resources. * The top level nav keys that have unread news resources.
*/ */
val topLevelRoutesWithUnreadResources: StateFlow<Set<NavKey>> = val topLevelNavKeysWithUnreadResources: StateFlow<Set<NavKey>> =
userNewsResourceRepository.observeAllForFollowedTopics() userNewsResourceRepository.observeAllForFollowedTopics()
.combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources -> .combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources ->
setOfNotNull( setOfNotNull(
ForYouRoute.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, ForYouNavKey.takeIf { forYouNewsResources.any { !it.hasBeenViewed } },
BookmarksRoute.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, BookmarksNavKey.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } },
) )
} }
.stateIn( .stateIn(
@ -115,15 +110,10 @@ class NiaAppState(
/** /**
* Stores information about navigation events to be used with JankStats * Stores information about navigation events to be used with JankStats
*/ */
// TODO: NavigationState needs to expose an observable representation of its state for this to work
@Composable @Composable
private fun NavigationTrackingSideEffect(navigationState: NavigationState) { private fun NavigationTrackingSideEffect(navigationState: NavigationState) {
// TrackDisposableJank(niaNavigator) { metricsHolder -> TrackDisposableJank(navigationState.currentKey) { metricsHolder ->
// snapshotFlow { metricsHolder.state?.putState("Navigation", navigationState.currentKey.toString())
// val stack = niaNavigator.backStack.toList() onDispose {}
// metricsHolder.state?.putState("Navigation", stack.lastOrNull().toString()) }
// }
// onDispose { }
// }
} }

@ -125,11 +125,9 @@ class NiaAppScreenSizesScreenshotTests {
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
niaNavigator = mockNiaBackStack(),
) )
NiaApp( NiaApp(
fakeAppState, fakeAppState,
entryProviderBuilders = MockEntryProvider,
windowAdaptiveInfo = WindowAdaptiveInfo( windowAdaptiveInfo = WindowAdaptiveInfo(
windowSizeClass = WindowSizeClass.compute( windowSizeClass = WindowSizeClass.compute(
width.value, width.value,

@ -18,14 +18,17 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.navigation3.runtime.NavBackStack
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.navigation.NavigationState
import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor 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.core.testing.util.TestTimeZoneMonitor
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey
import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -39,7 +42,6 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
/** /**
* Tests [NiaAppState]. * Tests [NiaAppState].
@ -63,31 +65,42 @@ class NiaAppStateTest {
// Subject under test. // Subject under test.
private lateinit var state: NiaAppState private lateinit var state: NiaAppState
private fun testNavigationState() = NavigationState(
startKey = ForYouNavKey,
topLevelStack = NavBackStack(ForYouNavKey),
subStacks = mapOf(
ForYouNavKey to NavBackStack(ForYouNavKey),
BookmarksNavKey to NavBackStack(BookmarksNavKey),
),
)
@Test @Test
fun niaAppState_currentDestination() = runTest { fun niaAppState_currentDestination() = runTest {
val niaBackStack = mockNiaBackStack() val navigationState = testNavigationState()
val navigator = Navigator(navigationState)
composeTestRule.setContent { composeTestRule.setContent {
state = remember(niaBackStack) { state = remember(navigationState) {
NiaAppState( NiaAppState(
niaNavigator = niaBackStack,
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
navigationState = navigationState,
) )
} }
} }
assertEquals(ForYouRoute, state.niaNavigator.currentActiveTopLevelKey) assertEquals(ForYouNavKey, state.navigationState.currentTopLevelKey)
assertEquals(ForYouRoute, state.niaNavigator.currentKey) assertEquals(ForYouNavKey, state.navigationState.currentKey)
// Navigate to another destination once // Navigate to another destination once
niaBackStack.navigate(BookmarksRoute) navigator.navigate(BookmarksNavKey)
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
assertEquals(BookmarksRoute, state.niaNavigator.currentActiveTopLevelKey) assertEquals(BookmarksNavKey, state.navigationState.currentTopLevelKey)
assertEquals(BookmarksRoute, state.niaNavigator.currentKey) assertEquals(BookmarksNavKey, state.navigationState.currentKey)
} }
@Test @Test
@ -97,14 +110,16 @@ class NiaAppStateTest {
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
niaNavigator = mockNiaBackStack(),
) )
} }
assertEquals(3, TOP_LEVEL_NAV_ITEMS.size) val navigationState = state.navigationState
assertTrue(TOP_LEVEL_NAV_ITEMS[0].name.contains("for_you", true))
assertTrue(TOP_LEVEL_NAV_ITEMS[1].name.contains("bookmarks", true)) assertEquals(3, navigationState.topLevelKeys.size)
assertTrue(TOP_LEVEL_NAV_ITEMS[2].name.contains("interests", true)) assertEquals(
setOf(ForYouNavKey, BookmarksNavKey, InterestsNavKey),
navigationState.topLevelKeys,
)
} }
@Test @Test
@ -115,7 +130,7 @@ class NiaAppStateTest {
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
niaNavigator = mockNiaBackStack(), navigationState = testNavigationState(),
) )
} }
@ -135,7 +150,7 @@ class NiaAppStateTest {
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
niaNavigator = mockNiaBackStack(), navigationState = testNavigationState(),
) )
} }
val changedTz = TimeZone.of("Europe/Prague") val changedTz = TimeZone.of("Europe/Prague")

@ -250,11 +250,9 @@ class SnackbarInsetsScreenshotTests {
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
niaNavigator = mockNiaBackStack(),
) )
NiaApp( NiaApp(
appState = appState, appState = appState,
entryProviderBuilders = MockEntryProvider,
showSettingsDialog = false, showSettingsDialog = false,
onSettingsDismissed = {}, onSettingsDismissed = {},
onTopAppBarActionClick = {}, onTopAppBarActionClick = {},

@ -200,11 +200,9 @@ class SnackbarScreenshotTests {
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
niaNavigator = mockNiaBackStack(),
) )
NiaApp( NiaApp(
appState = appState, appState = appState,
entryProviderBuilders = MockEntryProvider,
showSettingsDialog = false, showSettingsDialog = false,
onSettingsDismissed = {}, onSettingsDismissed = {},
onTopAppBarActionClick = {}, onTopAppBarActionClick = {},

@ -1,36 +0,0 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.ui
import androidx.navigation3.runtime.EntryProviderScope
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
val MockEntryProvider: Set<EntryProviderScope<NiaNavKey>.() -> Unit> =
setOf(
{
entry<ForYouRoute> {
ForYouScreen({})
}
},
)
private val startKey = ForYouRoute
fun mockNiaBackStack() = NiaNavigator(startKey)

@ -36,9 +36,8 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
@Composable @Composable
fun rememberNavigationState( fun rememberNavigationState(
startKey: NavKey, startKey: NavKey,
topLevelKeys: Set<NavKey> topLevelKeys: Set<NavKey>,
): NavigationState { ): NavigationState {
val topLevelStack = rememberNavBackStack(startKey) val topLevelStack = rememberNavBackStack(startKey)
val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) } val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) }
@ -46,7 +45,7 @@ fun rememberNavigationState(
NavigationState( NavigationState(
startKey = startKey, startKey = startKey,
topLevelStack = topLevelStack, topLevelStack = topLevelStack,
subStacks = subStacks subStacks = subStacks,
) )
} }
} }
@ -61,7 +60,7 @@ fun rememberNavigationState(
class NavigationState( class NavigationState(
val startKey: NavKey, val startKey: NavKey,
val topLevelStack: NavBackStack<NavKey>, val topLevelStack: NavBackStack<NavKey>,
val subStacks: Map<NavKey, NavBackStack<NavKey>> val subStacks: Map<NavKey, NavBackStack<NavKey>>,
) { ) {
val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() } val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() }
@ -74,8 +73,7 @@ class NavigationState(
?: error("Sub stack for $currentTopLevelKey does not exist") ?: error("Sub stack for $currentTopLevelKey does not exist")
@get:VisibleForTesting @get:VisibleForTesting
val currentKey val currentKey: NavKey by derivedStateOf { currentSubStack.last() }
get() = currentSubStack.last()
} }
/** /**
@ -83,9 +81,8 @@ class NavigationState(
*/ */
@Composable @Composable
fun NavigationState.toEntries( fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey> entryProvider: (NavKey) -> NavEntry<NavKey>,
): SnapshotStateList<NavEntry<NavKey>> { ): SnapshotStateList<NavEntry<NavKey>> {
val decoratedEntries = subStacks.mapValues { (_, stack) -> val decoratedEntries = subStacks.mapValues { (_, stack) ->
val decorators = listOf( val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(), rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
@ -93,7 +90,7 @@ fun NavigationState.toEntries(
rememberDecoratedNavEntries( rememberDecoratedNavEntries(
backStack = stack, backStack = stack,
entryDecorators = decorators, entryDecorators = decorators,
entryProvider = entryProvider entryProvider = entryProvider,
) )
} }

@ -36,20 +36,19 @@ class NavigatorTest {
@Before @Before
fun setup() { fun setup() {
val startKey = TestFirstTopLevelKey val startKey = TestFirstTopLevelKey
val topLevelStack = NavBackStack<NavKey>(startKey) val topLevelStack = NavBackStack<NavKey>(startKey)
val topLevelKeys = listOf( val topLevelKeys = listOf(
startKey, startKey,
TestSecondTopLevelKey, TestSecondTopLevelKey,
TestThirdTopLevelKey TestThirdTopLevelKey,
) )
val subStacks = topLevelKeys.associateWith { key -> NavBackStack(key) } val subStacks = topLevelKeys.associateWith { key -> NavBackStack(key) }
navigationState = NavigationState( navigationState = NavigationState(
startKey = startKey, startKey = startKey,
topLevelStack = topLevelStack, topLevelStack = topLevelStack,
subStacks = subStacks subStacks = subStacks,
) )
navigator = Navigator(navigationState) navigator = Navigator(navigationState)
} }
@ -66,7 +65,6 @@ class NavigatorTest {
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
assertThat(navigationState.subStacks[TestFirstTopLevelKey]?.last()).isEqualTo(TestKeyFirst) assertThat(navigationState.subStacks[TestFirstTopLevelKey]?.last()).isEqualTo(TestKeyFirst)
} }
@Test @Test
@ -256,5 +254,3 @@ class NavigatorTest {
} }
} }
} }

@ -1,3 +0,0 @@
# :feature:bookmarks:api module
## Dependency graph
![Dependency graph](../../../docs/images/graphs/dep_graph_feature_bookmarks_api.svg)

@ -20,4 +20,4 @@ import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
object BookmarksRoute : NavKey object BookmarksNavKey : NavKey

@ -1,3 +0,0 @@
# :feature:bookmarks:impl module
## Dependency graph
![Dependency graph](../../../docs/images/graphs/dep_graph_feature_bookmarks_impl.svg)

@ -23,12 +23,12 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksScreen 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.navigateToTopic
fun EntryProviderScope<NavKey>.bookmarksEntry(navigator: Navigator) { fun EntryProviderScope<NavKey>.bookmarksEntry(navigator: Navigator) {
entry<BookmarksRoute> { entry<BookmarksNavKey> {
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
BookmarksScreen( BookmarksScreen(
onTopicClick = navigator::navigateToTopic, onTopicClick = navigator::navigateToTopic,
@ -43,6 +43,7 @@ fun EntryProviderScope<NavKey>.bookmarksEntry(navigator: Navigator) {
} }
} }
// TODO: Why is this here?
val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState> { val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState> {
error("SnackbarHostState state should be initialized at runtime") error("SnackbarHostState state should be initialized at runtime")
} }

@ -1,3 +0,0 @@
# :feature:foryou:api module
## Dependency graph
![Dependency graph](../../../docs/images/graphs/dep_graph_feature_foryou_api.svg)

@ -20,4 +20,4 @@ import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
object ForYouRoute : NavKey object ForYouNavKey : NavKey

@ -1,3 +0,0 @@
# :feature:foryou:impl module
## Dependency graph
![Dependency graph](../../../docs/images/graphs/dep_graph_feature_foryou_impl.svg)

@ -20,7 +20,6 @@ import android.net.Uri
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
import androidx.activity.compose.ReportDrawnWhen import androidx.activity.compose.ReportDrawnWhen
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -107,8 +106,7 @@ import com.google.samples.apps.nowinandroid.core.ui.newsFeed
import com.google.samples.apps.nowinandroid.feature.foryou.api.R import com.google.samples.apps.nowinandroid.feature.foryou.api.R
@Composable @Composable
@VisibleForTesting fun ForYouScreen(
public fun ForYouScreen(
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ForYouViewModel = hiltViewModel(), viewModel: ForYouViewModel = hiltViewModel(),

@ -19,17 +19,14 @@ package com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey
import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen 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.navigateToTopic
fun EntryProviderScope<NavKey>.forYouEntry(navigator: Navigator) { fun EntryProviderScope<NavKey>.forYouEntry(navigator: Navigator) {
entry<ForYouRoute> { entry<ForYouNavKey> {
ForYouScreen( ForYouScreen(
onTopicClick = navigator::navigateToTopic, onTopicClick = navigator::navigateToTopic,
) )
} }
} }

@ -50,7 +50,7 @@ import java.util.TimeZone
*/ */
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [35]) @Config(application = HiltTestApplication::class)
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class ForYouScreenScreenshotTests { class ForYouScreenScreenshotTests {

@ -20,7 +20,7 @@ import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class InterestsRoute( data class InterestsNavKey(
// The ID of the topic which will be initially selected at this destination // The ID of the topic which will be initially selected at this destination
val initialTopicId: String? = null, val initialTopicId: String? = null,
) : NavKey ) : NavKey

@ -23,7 +23,7 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserDataReposit
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField import com.google.samples.apps.nowinandroid.core.domain.TopicSortField
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -40,7 +40,7 @@ class InterestsViewModel @AssistedInject constructor(
val userDataRepository: UserDataRepository, val userDataRepository: UserDataRepository,
getFollowableTopics: GetFollowableTopicsUseCase, getFollowableTopics: GetFollowableTopicsUseCase,
// TODO: see comment below // TODO: see comment below
@Assisted val key: InterestsRoute, @Assisted val key: InterestsNavKey,
) : ViewModel() { ) : ViewModel() {
// TODO: this should no longer be necessary, the currently selected topic should be // TODO: this should no longer be necessary, the currently selected topic should be
@ -77,7 +77,7 @@ class InterestsViewModel @AssistedInject constructor(
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(key: InterestsRoute): InterestsViewModel fun create(key: InterestsNavKey): InterestsViewModel
} }
} }

@ -22,34 +22,15 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsDetailPlaceholder 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.InterestsScreen
import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
//import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.multibindings.IntoSet
@Module
@InstallIn(ActivityComponent::class)
object InterestsEntryProvider {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Provides
@IntoSet
fun provideInterestsEntryProviderBuilder(
navigator: Navigator,
): EntryProviderScope<NavKey>.() -> Unit = { interestsEntry(navigator) }
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class) @OptIn(ExperimentalMaterial3AdaptiveApi::class)
fun EntryProviderScope<NavKey>.interestsEntry(navigator: Navigator) { fun EntryProviderScope<NavKey>.interestsEntry(navigator: Navigator) {
entry<InterestsRoute>( entry<InterestsNavKey>(
metadata = ListDetailSceneStrategy.listPane { metadata = ListDetailSceneStrategy.listPane {
InterestsDetailPlaceholder() InterestsDetailPlaceholder()
}, },

@ -18,10 +18,10 @@
package com.google.samples.apps.nowinandroid.interests.impl package com.google.samples.apps.nowinandroid.interests.impl
import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
@ -29,35 +29,26 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackViewModel import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator import com.google.samples.apps.nowinandroid.core.navigation.toEntries
import com.google.samples.apps.nowinandroid.feature.interests.api.R import com.google.samples.apps.nowinandroid.feature.interests.api.R
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import com.google.samples.apps.nowinandroid.feature.interests.impl.LIST_PANE_TEST_TAG import com.google.samples.apps.nowinandroid.feature.interests.impl.LIST_PANE_TEST_TAG
import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry
import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.Module
import dagger.Provides
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.modules.PolymorphicModuleBuilder
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -65,7 +56,6 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
import kotlin.getValue import kotlin.getValue
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
@ -83,13 +73,6 @@ class InterestsListDetailScreenTest {
@get:Rule(order = 1) @get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>() val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
// entry point to get the features' hilt-injected EntryProviders that are installed in ActivityComponent
@EntryPoint
@InstallIn(ActivityComponent::class)
interface EntryProvidersEntryPoint {
fun getEntryProviders(): Set<@JvmSuppressWildcards EntryProviderScope<NiaNavKey>.() -> Unit>
}
@Inject @Inject
lateinit var topicsRepository: TopicsRepository lateinit var topicsRepository: TopicsRepository
@ -104,15 +87,9 @@ class InterestsListDetailScreenTest {
private val Topic.testTag private val Topic.testTag
get() = "topic:${this.id}" get() = "topic:${this.id}"
private lateinit var entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>
@Before @Before
fun setup() { fun setup() {
hiltRule.inject() hiltRule.inject()
composeTestRule.apply {
entryProviderBuilders = EntryPoints.get(activity, EntryProvidersEntryPoint::class.java)
.getEntryProviders()
}
} }
@Test @Test
@ -121,13 +98,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
NiaTheme { NiaTheme {
NavDisplay( TestNavDisplay()
backStack = listOf<NiaNavKey>(InterestsRoute()),
sceneStrategy = rememberListDetailSceneStrategy(),
entryProvider = entryProvider {
entryProviderBuilders.forEach { it() }
},
)
} }
} }
onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed()
@ -135,19 +106,36 @@ class InterestsListDetailScreenTest {
} }
} }
@Composable
private fun TestNavDisplay() {
val startKey = InterestsNavKey(null)
val navigationState = rememberNavigationState(
startKey = startKey,
topLevelKeys = setOf(startKey),
)
val navigator = Navigator(navigationState)
val entryProvider = entryProvider {
interestsEntry(navigator)
topicEntry(navigator)
}
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategy = rememberListDetailSceneStrategy(),
)
}
@Test @Test
@Config(qualifiers = COMPACT_WIDTH) @Config(qualifiers = COMPACT_WIDTH)
fun compactWidth_initialState_showsListPane() { fun compactWidth_initialState_showsListPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
NiaTheme { NiaTheme {
NavDisplay( TestNavDisplay()
backStack = listOf<NiaNavKey>(InterestsRoute()),
sceneStrategy = rememberListDetailSceneStrategy(),
entryProvider = entryProvider {
entryProviderBuilders.forEach { it() }
},
)
} }
} }
@ -161,17 +149,8 @@ class InterestsListDetailScreenTest {
fun expandedWidth_topicSelected_updatesDetailPane() { fun expandedWidth_topicSelected_updatesDetailPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>()
// TODO: This is broken
val backStack = backStackViewModel.niaNavigator.backStack
NiaTheme { NiaTheme {
NavDisplay( TestNavDisplay()
backStack = backStack,
sceneStrategy = rememberListDetailSceneStrategy(),
entryProvider = entryProvider {
entryProviderBuilders.forEach { it() }
},
)
} }
} }
val firstTopic = getTopics().first() val firstTopic = getTopics().first()
@ -189,16 +168,8 @@ class InterestsListDetailScreenTest {
fun compactWidth_topicSelected_showsTopicDetailPane() { fun compactWidth_topicSelected_showsTopicDetailPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>()
val backStack = backStackViewModel.niaNavigator.backStack
NiaTheme { NiaTheme {
NavDisplay( TestNavDisplay()
backStack = backStack,
sceneStrategy = rememberListDetailSceneStrategy(),
entryProvider = entryProvider {
entryProviderBuilders.forEach { it() }
},
)
} }
} }
@ -216,16 +187,8 @@ class InterestsListDetailScreenTest {
fun compactWidth_backPressFromTopicDetail_showsListPane() { fun compactWidth_backPressFromTopicDetail_showsListPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>()
val backStack = backStackViewModel.niaNavigator.backStack
NiaTheme { NiaTheme {
NavDisplay( TestNavDisplay()
backStack = backStack,
sceneStrategy = rememberListDetailSceneStrategy(),
entryProvider = entryProvider {
entryProviderBuilders.forEach { it() }
},
)
} }
} }
@ -246,22 +209,3 @@ private fun AndroidComposeTestRule<*, *>.stringResource(
@StringRes resId: Int, @StringRes resId: Int,
): ReadOnlyProperty<Any, String> = ): ReadOnlyProperty<Any, String> =
ReadOnlyProperty { _, _ -> activity.getString(resId) } ReadOnlyProperty { _, _ -> activity.getString(resId) }
@Module
@InstallIn(SingletonComponent::class)
object BackStackProvider {
@Provides
@Singleton
fun provideNiaBackStack(): NiaNavigator =
NiaNavigator(startKey = InterestsRoute())
@Provides
@Singleton
fun provideSerializersModule(
polymorphicModuleBuilders: Set<@JvmSuppressWildcards PolymorphicModuleBuilder<NiaNavKey>.() -> Unit>,
): SerializersModule = SerializersModule {
polymorphic(NiaNavKey::class) {
polymorphicModuleBuilders.forEach { it() }
}
}
}

@ -24,7 +24,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsUiState import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsUiState
import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -68,11 +68,11 @@ class InterestsViewModelTest {
fun setup() { fun setup() {
viewModel = InterestsViewModel( viewModel = InterestsViewModel(
savedStateHandle = SavedStateHandle( savedStateHandle = SavedStateHandle(
route = InterestsRoute(initialTopicId = testInputTopics[0].topic.id), route = InterestsNavKey(initialTopicId = testInputTopics[0].topic.id),
), ),
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
getFollowableTopics = getFollowableTopicsUseCase, getFollowableTopics = getFollowableTopicsUseCase,
InterestsRoute(initialTopicId = testInputTopics[0].topic.id), InterestsNavKey(initialTopicId = testInputTopics[0].topic.id),
) )
} }

@ -20,4 +20,4 @@ import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
object SearchRoute : NavKey object SearchNavKey : NavKey

@ -1,5 +1,5 @@
/* /*
* Copyright 2022 The Android Open Source Project * Copyright 2023 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

@ -19,16 +19,16 @@ package com.google.samples.apps.nowinandroid.feature.search.impl.navigation
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRoute import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchScreen 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.topic.api.navigation.navigateToTopic
fun EntryProviderScope<NavKey>.searchEntry(navigator: Navigator) { fun EntryProviderScope<NavKey>.searchEntry(navigator: Navigator) {
entry<SearchRoute> { entry<SearchNavKey> {
SearchScreen( SearchScreen(
onBackClick = { navigator.goBack() }, onBackClick = { navigator.goBack() },
onInterestsClick = { navigator.navigate(InterestsRoute()) }, onInterestsClick = { navigator.navigate(InterestsNavKey()) },
onTopicClick = navigator::navigateToTopic, onTopicClick = navigator::navigateToTopic,
) )
} }

@ -21,10 +21,10 @@ import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class TopicRoute(val id: String) : NavKey data class TopicNavKey(val id: String) : NavKey
fun Navigator.navigateToTopic( fun Navigator.navigateToTopic(
topicId: String, topicId: String,
) { ) {
navigate(TopicRoute(topicId)) navigate(TopicNavKey(topicId))
} }

@ -22,7 +22,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicNavKey
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.TopicScreen
import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel
@ -30,7 +30,7 @@ import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel.Fa
@OptIn(ExperimentalMaterial3AdaptiveApi::class) @OptIn(ExperimentalMaterial3AdaptiveApi::class)
fun EntryProviderScope<NavKey>.topicEntry(navigator: Navigator) { fun EntryProviderScope<NavKey>.topicEntry(navigator: Navigator) {
entry<TopicRoute>( entry<TopicNavKey>(
metadata = ListDetailSceneStrategy.detailPane(), metadata = ListDetailSceneStrategy.detailPane(),
) { key -> ) { key ->
val id = key.id val id = key.id

@ -40,9 +40,6 @@ dependencyResolutionManagement {
} }
} }
mavenCentral() mavenCentral()
maven {
url = uri("https://androidx.dev/snapshots/builds/14161874/artifacts/repository")
}
} }
} }
rootProject.name = "nowinandroid" rootProject.name = "nowinandroid"

Loading…
Cancel
Save