Refactor Interests to ListDetailScene

pull/1902/head
Clara Fok 2 months ago
parent 04fa1ff2b7
commit 67d79782a9

@ -84,24 +84,24 @@ dependencies {
implementation(projects.core.designsystem) implementation(projects.core.designsystem)
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.model) implementation(projects.core.model)
implementation(projects.core.navigation)
implementation(projects.core.analytics) implementation(projects.core.analytics)
implementation(projects.core.navigation) implementation(projects.core.navigation)
implementation(projects.sync.work) implementation(projects.sync.work)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation) implementation(libs.androidx.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.adaptive.navigation3)
implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.material3.windowSizeClass)
implementation(libs.androidx.compose.runtime.tracing) implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.lifecycle.viewModel.navigation3)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.tracing.ktx)

@ -43,6 +43,7 @@ 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.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import com.google.samples.apps.nowinandroid.ui.NiaApp import com.google.samples.apps.nowinandroid.ui.NiaApp
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme
@ -80,7 +81,7 @@ class MainActivity : ComponentActivity() {
lateinit var niaBackStack: NiaBackStack lateinit var niaBackStack: NiaBackStack
@Inject @Inject
lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit> lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()

@ -26,7 +26,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object NiaAppNavigation { object BackStackProvider {
@Provides @Provides
@Singleton @Singleton
fun provideNiaBackStack(): NiaBackStack = fun provideNiaBackStack(): NiaBackStack =

@ -16,23 +16,36 @@
package com.google.samples.apps.nowinandroid.navigation package com.google.samples.apps.nowinandroid.navigation
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.EntryProviderBuilder import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.NavEntryDecorator
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable @Composable
fun NiaNavDisplay( fun NiaNavDisplay(
niaBackStack: NiaBackStack, niaBackStack: NiaBackStack,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit>, entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>,
) { ) {
val listDetailStrategy = rememberListDetailSceneStrategy<NiaBackStackKey>()
NavDisplay( NavDisplay(
backStack = niaBackStack.backStack, backStack = niaBackStack.backStack,
sceneStrategy = listDetailStrategy,
onBack = { niaBackStack.removeLast() }, onBack = { niaBackStack.removeLast() },
entryDecorators = listOf(
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = entryProvider { entryProvider = entryProvider {
entryProviderBuilders.forEach { builder -> entryProviderBuilders.forEach { builder ->
builder() builder()

@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.bookmarksScreen import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.bookmarksScreen
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouBaseRoute import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.forYouSection import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.forYouSection
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.navigateToInterests import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.searchScreen import com.google.samples.apps.nowinandroid.feature.search.api.navigation.searchScreen
@ -46,7 +46,7 @@ fun NiaNavHost(
val navController = appState.navController val navController = appState.navController
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = ForYouBaseRoute, startDestination = ForYouRoute,
modifier = modifier, modifier = modifier,
) { ) {
forYouSection( forYouSection(

@ -20,8 +20,8 @@ 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.R
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouBaseRoute
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute 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.interests.api.navigation.InterestsRoute
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -49,8 +49,7 @@ enum class TopLevelDestination(
@StringRes val iconTextId: Int, @StringRes val iconTextId: Int,
@StringRes val titleTextId: Int, @StringRes val titleTextId: Int,
val route: KClass<*>, val route: KClass<*>,
val baseRoute: KClass<*> = route, val key: NiaBackStackKey
val key: Any
) { ) {
FOR_YOU( FOR_YOU(
selectedIcon = NiaIcons.Upcoming, selectedIcon = NiaIcons.Upcoming,
@ -58,8 +57,7 @@ enum class TopLevelDestination(
iconTextId = forYouR.string.feature_foryou_api_title, iconTextId = forYouR.string.feature_foryou_api_title,
titleTextId = R.string.app_name, titleTextId = R.string.app_name,
route = ForYouRoute::class, route = ForYouRoute::class,
baseRoute = ForYouBaseRoute::class, key = ForYouRoute
key = ForYouBaseRoute
), ),
BOOKMARKS( BOOKMARKS(
selectedIcon = NiaIcons.Bookmarks, selectedIcon = NiaIcons.Bookmarks,
@ -78,3 +76,5 @@ enum class TopLevelDestination(
key = InterestsRoute(null) key = InterestsRoute(null)
), ),
} }
internal val TopLevelDestinations = TopLevelDestination.entries.associateBy { dest -> dest.key }

@ -71,7 +71,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAp
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors 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.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
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.search.api.navigation.navigateToSearch
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.navigation.NiaNavDisplay import com.google.samples.apps.nowinandroid.navigation.NiaNavDisplay
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@ -81,7 +83,7 @@ import com.google.samples.apps.nowinandroid.feature.settings.api.R as settingsR
@Composable @Composable
fun NiaApp( fun NiaApp(
appState: NiaAppState, appState: NiaAppState,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit>, entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) { ) {
@ -132,7 +134,7 @@ fun NiaApp(
) )
internal fun NiaApp( internal fun NiaApp(
appState: NiaAppState, appState: NiaAppState,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit>, entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>,
showSettingsDialog: Boolean, showSettingsDialog: Boolean,
onSettingsDismissed: () -> Unit, onSettingsDismissed: () -> Unit,
onTopAppBarActionClick: () -> Unit, onTopAppBarActionClick: () -> Unit,
@ -141,7 +143,7 @@ internal fun NiaApp(
) { ) {
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources val unreadDestinations by appState.topLevelDestinationsWithUnreadResources
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
val currentTopLevelKey = appState.currentTopLevelDestination val currentTopLevelKey = appState.currentTopLevelDestination!!.key
if (showSettingsDialog) { if (showSettingsDialog) {
@ -156,10 +158,7 @@ internal fun NiaApp(
navigationSuiteItems = { navigationSuiteItems = {
appState.topLevelDestinations.forEach { destination -> appState.topLevelDestinations.forEach { destination ->
val hasUnread = unreadDestinations.contains(destination) val hasUnread = unreadDestinations.contains(destination)
// val selected = currentDestination
// .isRouteInHierarchy(destination.baseRoute)
val selected = destination.key == currentTopLevelKey val selected = destination.key == currentTopLevelKey
println("cfok destination:$destination, currentDest:$currentTopLevelKey")
item( item(
selected = selected, selected = selected,
onClick = { appState.navigateToTopLevelDestination(destination) }, onClick = { appState.navigateToTopLevelDestination(destination) },
@ -233,7 +232,7 @@ internal fun NiaApp(
containerColor = Color.Transparent, containerColor = Color.Transparent,
), ),
onActionClick = { onTopAppBarActionClick() }, onActionClick = { onTopAppBarActionClick() },
onNavigationClick = { appState.navigateToSearchNav3() }, onNavigationClick = { appState.niaBackStack.navigateToSearch() },
) )
} }

@ -35,6 +35,7 @@ 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.BOOKMARKS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestinations
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
@ -98,12 +99,7 @@ class NiaAppState(
// } // }
val currentTopLevelDestination: TopLevelDestination? val currentTopLevelDestination: TopLevelDestination?
@Composable get() { @Composable get() = TopLevelDestinations[niaBackStack.currentTopLevelKey]
return TopLevelDestination.entries.firstOrNull { topLevelDestination ->
topLevelDestination.key == niaBackStack.currentTopLevelKey
// currentDestination?.hasRoute(route = topLevelDestination.route) == true
}
}
val isOffline = networkMonitor.isOnline val isOffline = networkMonitor.isOnline
.map(Boolean::not) .map(Boolean::not)
@ -178,9 +174,6 @@ class NiaAppState(
} }
fun navigateToSearch() = navController.navigateToSearch() fun navigateToSearch() = navController.navigateToSearch()
fun navigateToSearchNav3() = niaBackStack.navigateToSearch(
onInterestsClick = { navigateToTopLevelDestination(INTERESTS) }
)
} }
/** /**

@ -4,5 +4,5 @@ plugins {
} }
dependencies { dependencies {
implementation(libs.androidx.navigation3.runtime) api(libs.androidx.navigation3.runtime)
} }

@ -16,7 +16,6 @@
package com.google.samples.apps.nowinandroid.core.navigation package com.google.samples.apps.nowinandroid.core.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -26,12 +25,12 @@ import javax.inject.Inject
import kotlin.collections.remove import kotlin.collections.remove
class NiaBackStack @Inject constructor( class NiaBackStack @Inject constructor(
startKey: Any, startKey: NiaBackStackKey,
) { ) {
val backStack = mutableStateListOf(startKey) val backStack = mutableStateListOf(startKey)
// Maintain a stack for each top level route // Maintain a stack for each top level route
private var topLevelStacks : LinkedHashMap<Any, SnapshotStateList<Any>> = linkedMapOf( private var topLevelStacks : LinkedHashMap<NiaBackStackKey, SnapshotStateList<NiaBackStackKey>> = linkedMapOf(
startKey to mutableStateListOf(startKey) startKey to mutableStateListOf(startKey)
) )
@ -39,8 +38,8 @@ class NiaBackStack @Inject constructor(
var currentTopLevelKey by mutableStateOf(startKey) var currentTopLevelKey by mutableStateOf(startKey)
private set private set
internal val currentKey: Any internal val currentKey: NiaBackStackKey
@Composable get() = topLevelStacks[currentTopLevelKey]!!.last() get() = topLevelStacks[currentTopLevelKey]!!.last()
private fun updateBackStack() = private fun updateBackStack() =
backStack.apply { backStack.apply {
@ -48,7 +47,7 @@ class NiaBackStack @Inject constructor(
addAll(topLevelStacks.flatMap { it.value }) addAll(topLevelStacks.flatMap { it.value })
} }
fun navigateToTopLevelDestination(key: Any){ fun navigateToTopLevelDestination(key: NiaBackStackKey){
// If the top level doesn't exist, add it // If the top level doesn't exist, add it
if (topLevelStacks[key] == null){ if (topLevelStacks[key] == null){
topLevelStacks.put(key, mutableStateListOf(key)) topLevelStacks.put(key, mutableStateListOf(key))
@ -60,15 +59,17 @@ class NiaBackStack @Inject constructor(
} }
} }
} }
currentTopLevelKey = key currentTopLevelKey = key
updateBackStack() updateBackStack()
} }
fun navigate(key: Any){ fun navigate(key: NiaBackStackKey){
println("cfok navigate $key") if (backStack.lastOrNull() != key) {
topLevelStacks[currentTopLevelKey]?.add(key) topLevelStacks[currentTopLevelKey]?.add(key)
updateBackStack() updateBackStack()
} }
}
fun removeLast(){ fun removeLast(){
val removedKey = topLevelStacks[currentTopLevelKey]?.removeLastOrNull() val removedKey = topLevelStacks[currentTopLevelKey]?.removeLastOrNull()
@ -77,5 +78,6 @@ class NiaBackStack @Inject constructor(
currentTopLevelKey = topLevelStacks.keys.last() currentTopLevelKey = topLevelStacks.keys.last()
updateBackStack() updateBackStack()
} }
} }
interface NiaBackStackKey

@ -21,3 +21,7 @@ plugins {
android { android {
namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks.api" namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks.api"
} }
dependencies {
api(projects.core.navigation)
}

@ -19,9 +19,10 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable object BookmarksRoute @Serializable object BookmarksRoute: NiaBackStackKey
fun NavController.navigateToBookmarks(navOptions: NavOptions) = fun NavController.navigateToBookmarks(navOptions: NavOptions) =
navigate(route = BookmarksRoute, navOptions) navigate(route = BookmarksRoute, navOptions)

@ -26,7 +26,6 @@ android {
dependencies { dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.feature.bookmarks.api) implementation(projects.feature.bookmarks.api)
implementation(projects.core.navigation)
implementation(projects.feature.topic.api) implementation(projects.feature.topic.api)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)

@ -35,6 +35,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner
import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -63,7 +64,7 @@ class BookmarksScreenTest {
composeTestRule composeTestRule
.onNodeWithContentDescription( .onNodeWithContentDescription(
composeTestRule.activity.resources.getString(R.string.feature_bookmarks_api_loading), composeTestRule.activity.resources.getString(R.string.feature_bookmarks_impl_loading),
) )
.assertExists() .assertExists()
} }
@ -160,13 +161,13 @@ class BookmarksScreenTest {
composeTestRule composeTestRule
.onNodeWithText( .onNodeWithText(
composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_error), composeTestRule.activity.getString(R.string.feature_bookmarks_impl_empty_error),
) )
.assertExists() .assertExists()
composeTestRule composeTestRule
.onNodeWithText( .onNodeWithText(
composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_description), composeTestRule.activity.getString(R.string.feature_bookmarks_impl_empty_description),
) )
.assertExists() .assertExists()
} }

@ -76,7 +76,7 @@ import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParam
import com.google.samples.apps.nowinandroid.core.ui.newsFeed import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@Composable @Composable
internal fun BookmarksRoute( internal fun BookmarksScreen(
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
onShowSnackbar: suspend (String, String?) -> Boolean, onShowSnackbar: suspend (String, String?) -> Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

@ -23,8 +23,9 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.navigation3.runtime.EntryProviderBuilder import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.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.navigateToTopic
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -40,10 +41,10 @@ object BookmarksModule {
@IntoSet @IntoSet
fun provideBookmarksEntryProviderBuilder( fun provideBookmarksEntryProviderBuilder(
backStack: NiaBackStack, backStack: NiaBackStack,
): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = { ): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<BookmarksRoute> { entry<BookmarksRoute> {
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
BookmarksRoute( BookmarksScreen(
onTopicClick = backStack::navigateToTopic, onTopicClick = backStack::navigateToTopic,
onShowSnackbar = { message, action -> onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
@ -58,5 +59,5 @@ object BookmarksModule {
} }
val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState> { val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState> {
error("host state should be initialzied at runtime") error("SnackbarHostState state should be initialized at runtime")
} }

@ -24,16 +24,5 @@ android {
} }
dependencies { dependencies {
implementation(libs.accompanist.permissions) api(projects.core.navigation)
implementation(projects.core.data)
implementation(projects.core.domain)
implementation(projects.core.notifications)
testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric)
testImplementation(projects.core.testing)
testDemoImplementation(projects.core.screenshotTesting)
androidTestImplementation(libs.bundles.androidx.compose.ui.test)
androidTestImplementation(projects.core.testing)
} }

@ -19,11 +19,10 @@ package com.google.samples.apps.nowinandroid.feature.foryou.api.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable data object ForYouRoute // route to ForYou screen @Serializable data object ForYouRoute: NiaBackStackKey // route to ForYou screen
@Serializable data object ForYouBaseRoute // route to base navigation graph
fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(route = ForYouRoute, navOptions) fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(route = ForYouRoute, navOptions)

@ -29,7 +29,6 @@ dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.domain) implementation(projects.core.domain)
implementation(projects.core.notifications) implementation(projects.core.notifications)
implementation(projects.core.navigation)
implementation(projects.feature.foryou.api) implementation(projects.feature.foryou.api)
implementation(projects.feature.topic.api) implementation(projects.feature.topic.api)

@ -19,7 +19,8 @@ package com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation
import androidx.navigation3.runtime.EntryProviderBuilder import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouBaseRoute import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
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.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
import dagger.Module import dagger.Module
@ -36,8 +37,8 @@ object ForYouModule {
@IntoSet @IntoSet
fun provideForYouEntryProviderBuilder( fun provideForYouEntryProviderBuilder(
backStack: NiaBackStack, backStack: NiaBackStack,
): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = { ): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<ForYouBaseRoute> { entry<ForYouRoute> {
ForYouScreen( ForYouScreen(
onTopicClick = backStack::navigateToTopic onTopicClick = backStack::navigateToTopic
) )

@ -23,5 +23,5 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.navigation) api(projects.core.navigation)
} }

@ -19,12 +19,13 @@ package com.google.samples.apps.nowinandroid.feature.interests.api.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable data class InterestsRoute( @Serializable data class InterestsRoute(
// 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,
) ): NiaBackStackKey
fun NavController.navigateToInterests( fun NavController.navigateToInterests(
initialTopicId: String? = null, initialTopicId: String? = null,

@ -32,6 +32,7 @@ dependencies {
implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation) implementation(libs.androidx.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.adaptive.navigation3)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)
testImplementation(libs.robolectric) testImplementation(libs.robolectric)

@ -16,11 +16,15 @@
package com.google.samples.apps.nowinandroid.feature.interests.impl package com.google.samples.apps.nowinandroid.feature.interests.impl
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.navigation.compose.composable import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.navigation3.runtime.EntryProviderBuilder import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.topic.api.TopicDetailPlaceholder
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -31,11 +35,22 @@ import dagger.multibindings.IntoSet
@InstallIn(ActivityComponent::class) @InstallIn(ActivityComponent::class)
object InterestsModule { object InterestsModule {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Provides @Provides
@IntoSet @IntoSet
fun provideInterestsEntryProviderBuilder(): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = { fun provideInterestsEntryProviderBuilder(
entry<InterestsRoute> { key -> backStack: NiaBackStack
InterestsListDetailScreen() ): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<InterestsRoute>(
metadata = ListDetailSceneStrategy.listPane {
TopicDetailPlaceholder()
}
) { key ->
// InterestsListDetailScreen()
InterestsScreen(
onTopicClick = backStack::navigateToTopic,
shouldHighlightSelectedTopic = false,
)
} }
} }
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2022 The Android Open Source Project * Copyright 2024 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.
@ -165,7 +165,7 @@ internal fun InterestsListDetailScreen(
} }
}, },
) { ) {
InterestsRoute( InterestsScreen(
onTopicClick = ::onTopicClickShowDetailPane, onTopicClick = ::onTopicClickShowDetailPane,
shouldHighlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(), shouldHighlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
) )

@ -36,7 +36,7 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.feature.interests.api.R import com.google.samples.apps.nowinandroid.feature.interests.api.R
@Composable @Composable
fun InterestsRoute( fun InterestsScreen(
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
shouldHighlightSelectedTopic: Boolean = false, shouldHighlightSelectedTopic: Boolean = false,

@ -29,6 +29,7 @@ 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.feature.interests.impl.InterestsListDetailScreen
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
@ -86,7 +87,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
NiaTheme { NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen() InterestsListDetailScreen()
} }
} }
@ -101,7 +102,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
NiaTheme { NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen() InterestsListDetailScreen()
} }
} }
@ -116,7 +117,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
NiaTheme { NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen() InterestsListDetailScreen()
} }
} }
@ -135,7 +136,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
NiaTheme { NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen() InterestsListDetailScreen()
} }
} }
@ -160,7 +161,7 @@ class InterestsListDetailScreenTest {
BackHandler { BackHandler {
unhandledBackPress = true unhandledBackPress = true
} }
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen() InterestsListDetailScreen()
} }
} }
@ -180,7 +181,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
NiaTheme { NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen() InterestsListDetailScreen()
} }
} }

@ -25,7 +25,7 @@ android {
dependencies { dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.domain) implementation(projects.core.domain)
implementation(projects.core.navigation) api(projects.core.navigation)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)

@ -20,16 +20,15 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable data object SearchRoute @Serializable data object SearchRoute
@Serializable data class SearchRouteNav3(val onInterestsClick: () -> Unit) @Serializable object SearchRouteNav3: NiaBackStackKey
fun NiaBackStack.navigateToSearch( fun NiaBackStack.navigateToSearch() {
onInterestsClick: () -> Unit, navigate(SearchRouteNav3)
) {
navigate(SearchRouteNav3(onInterestsClick))
} }
fun NavController.navigateToSearch(navOptions: NavOptions? = null) = fun NavController.navigateToSearch(navOptions: NavOptions? = null) =

@ -27,9 +27,9 @@ android {
dependencies { dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.domain) implementation(projects.core.domain)
implementation(projects.core.navigation)
implementation(projects.feature.interests.api) implementation(projects.feature.interests.api)
implementation(projects.feature.search.api) implementation(projects.feature.search.api)
implementation(projects.feature.topic.api)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)

@ -93,14 +93,10 @@ import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
import com.google.samples.apps.nowinandroid.core.ui.R.string import com.google.samples.apps.nowinandroid.core.ui.R.string
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.newsFeed import com.google.samples.apps.nowinandroid.core.ui.newsFeed
import com.google.samples.apps.nowinandroid.feature.search.impl.RecentSearchQueriesUiState.Loading
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.EmptyQuery
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.LoadFailed
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.SearchNotReady
import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR
@Composable @Composable
internal fun SearchRoute( internal fun SearchScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onInterestsClick: () -> Unit, onInterestsClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
@ -131,7 +127,7 @@ internal fun SearchRoute(
internal fun SearchScreen( internal fun SearchScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
searchQuery: String = "", searchQuery: String = "",
recentSearchesUiState: RecentSearchQueriesUiState = Loading, recentSearchesUiState: RecentSearchQueriesUiState = RecentSearchQueriesUiState.Loading,
searchResultUiState: SearchResultUiState = SearchResultUiState.Loading, searchResultUiState: SearchResultUiState = SearchResultUiState.Loading,
onSearchQueryChanged: (String) -> Unit = {}, onSearchQueryChanged: (String) -> Unit = {},
onSearchTriggered: (String) -> Unit = {}, onSearchTriggered: (String) -> Unit = {},
@ -154,11 +150,11 @@ internal fun SearchScreen(
) )
when (searchResultUiState) { when (searchResultUiState) {
SearchResultUiState.Loading, SearchResultUiState.Loading,
LoadFailed, SearchResultUiState.LoadFailed,
-> Unit -> Unit
SearchNotReady -> SearchNotReadyBody() SearchResultUiState.SearchNotReady -> SearchNotReadyBody()
EmptyQuery, SearchResultUiState.EmptyQuery,
-> { -> {
if (recentSearchesUiState is RecentSearchQueriesUiState.Success) { if (recentSearchesUiState is RecentSearchQueriesUiState.Success) {
RecentSearchesBody( RecentSearchesBody(

@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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.core.ui.PreviewParameterData.newsResources import com.google.samples.apps.nowinandroid.core.ui.PreviewParameterData.newsResources
import com.google.samples.apps.nowinandroid.core.ui.PreviewParameterData.topics import com.google.samples.apps.nowinandroid.core.ui.PreviewParameterData.topics
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.Success
/** /**
* This [PreviewParameterProvider](https://developer.android.com/reference/kotlin/androidx/compose/ui/tooling/preview/PreviewParameterProvider) * This [PreviewParameterProvider](https://developer.android.com/reference/kotlin/androidx/compose/ui/tooling/preview/PreviewParameterProvider)
@ -30,7 +29,7 @@ import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiSt
*/ */
class SearchUiStatePreviewParameterProvider : PreviewParameterProvider<SearchResultUiState> { class SearchUiStatePreviewParameterProvider : PreviewParameterProvider<SearchResultUiState> {
override val values: Sequence<SearchResultUiState> = sequenceOf( override val values: Sequence<SearchResultUiState> = sequenceOf(
Success( SearchResultUiState.Success(
topics = topics.mapIndexed { i, topic -> topics = topics.mapIndexed { i, topic ->
FollowableTopic(topic = topic, isFollowed = i % 2 == 0) FollowableTopic(topic = topic, isFollowed = i % 2 == 0)
}, },

@ -28,11 +28,6 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserDataReposit
import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase
import com.google.samples.apps.nowinandroid.core.model.data.UserSearchResult import com.google.samples.apps.nowinandroid.core.model.data.UserSearchResult
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.EmptyQuery
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.LoadFailed
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.Loading
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.SearchNotReady
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.Success
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -61,29 +56,29 @@ class SearchViewModel @Inject constructor(
searchContentsRepository.getSearchContentsCount() searchContentsRepository.getSearchContentsCount()
.flatMapLatest { totalCount -> .flatMapLatest { totalCount ->
if (totalCount < SEARCH_MIN_FTS_ENTITY_COUNT) { if (totalCount < SEARCH_MIN_FTS_ENTITY_COUNT) {
flowOf(SearchNotReady) flowOf(SearchResultUiState.SearchNotReady)
} else { } else {
searchQuery.flatMapLatest { query -> searchQuery.flatMapLatest { query ->
if (query.trim().length < SEARCH_QUERY_MIN_LENGTH) { if (query.trim().length < SEARCH_QUERY_MIN_LENGTH) {
flowOf(EmptyQuery) flowOf(SearchResultUiState.EmptyQuery)
} else { } else {
getSearchContentsUseCase(query) getSearchContentsUseCase(query)
// Not using .asResult() here, because it emits Loading state every // Not using .asResult() here, because it emits Loading state every
// time the user types a letter in the search box, which flickers the screen. // time the user types a letter in the search box, which flickers the screen.
.map<UserSearchResult, SearchResultUiState> { data -> .map<UserSearchResult, SearchResultUiState> { data ->
Success( SearchResultUiState.Success(
topics = data.topics, topics = data.topics,
newsResources = data.newsResources, newsResources = data.newsResources,
) )
} }
.catch { emit(LoadFailed) } .catch { emit(SearchResultUiState.LoadFailed) }
} }
} }
} }
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading, initialValue = SearchResultUiState.Loading,
) )
val recentSearchQueriesUiState: StateFlow<RecentSearchQueriesUiState> = val recentSearchQueriesUiState: StateFlow<RecentSearchQueriesUiState> =

@ -16,13 +16,14 @@
package com.google.samples.apps.nowinandroid.feature.search.impl.navigation package com.google.samples.apps.nowinandroid.feature.search.impl.navigation
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.navigation3.runtime.EntryProviderBuilder import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.navigateToInterests import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRouteNav3 import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRouteNav3
import com.google.samples.apps.nowinandroid.feature.search.impl.SearchRoute import com.google.samples.apps.nowinandroid.feature.search.impl.SearchScreen
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -37,12 +38,12 @@ object SearchModule {
@IntoSet @IntoSet
fun provideSearchEntryProviderBuilder( fun provideSearchEntryProviderBuilder(
backStack: NiaBackStack, backStack: NiaBackStack,
): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = { ): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<SearchRouteNav3> { key -> entry<SearchRouteNav3> { key ->
SearchRoute( SearchScreen(
onBackClick = backStack::removeLast, onBackClick = backStack::removeLast,
onInterestsClick = key.onInterestsClick, onInterestsClick = { backStack.navigateToTopLevelDestination(InterestsRoute()) },
onTopicClick = backStack::navigateToInterests, onTopicClick = backStack::navigateToTopic,
) )
} }
} }

@ -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.
@ -102,7 +102,7 @@ class SearchViewModelTest {
searchContentsRepository.addTopics(topicsTestData) searchContentsRepository.addTopics(topicsTestData)
val result = viewModel.searchResultUiState.value val result = viewModel.searchResultUiState.value
assertIs<com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.Success>(result) assertIs<SearchResultUiState.Success>(result)
} }
@Test @Test

@ -24,10 +24,10 @@ android {
} }
dependencies { dependencies {
api(projects.core.navigation)
implementation(projects.core.data) implementation(projects.core.data)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)
implementation(projects.core.navigation)
testImplementation(libs.robolectric) testImplementation(libs.robolectric)
androidTestImplementation(libs.bundles.androidx.compose.ui.test) androidTestImplementation(libs.bundles.androidx.compose.ui.test)

@ -23,11 +23,12 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.toRoute import androidx.navigation.toRoute
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import com.google.samples.apps.nowinandroid.feature.topic.api.TopicViewModel import com.google.samples.apps.nowinandroid.feature.topic.api.TopicViewModel
import com.google.samples.apps.nowinandroid.feature.topic.api.TopicScreen import com.google.samples.apps.nowinandroid.feature.topic.api.TopicScreen
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable data class TopicRoute(val id: String) @Serializable data class TopicRoute(val id: String): NiaBackStackKey
fun NiaBackStack.navigateToTopic( fun NiaBackStack.navigateToTopic(
topicId: String, topicId: String,

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
~ Copyright 2024 The Android Open Source Project Copyright 2024 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.
~ You may obtain a copy of the License at You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0 https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and See the License for the specific language governing permissions and
~ limitations under the License. limitations under the License.
--> -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp" android:width="64dp"

@ -26,9 +26,10 @@ android {
dependencies { dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.navigation)
implementation(projects.feature.topic.api) implementation(projects.feature.topic.api)
implementation(libs.androidx.compose.material3.adaptive.navigation3)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)
testImplementation(libs.robolectric) testImplementation(libs.robolectric)

@ -16,11 +16,15 @@
package com.google.samples.apps.nowinandroid.feature.topic.impl.navigation package com.google.samples.apps.nowinandroid.feature.topic.impl.navigation
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPaneScope
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation3.runtime.EntryProviderBuilder import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute 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.TopicScreen import com.google.samples.apps.nowinandroid.feature.topic.api.TopicScreen
@ -36,12 +40,15 @@ import dagger.multibindings.IntoSet
@InstallIn(ActivityComponent::class) @InstallIn(ActivityComponent::class)
object TopicModule { object TopicModule {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Provides @Provides
@IntoSet @IntoSet
fun provideTopicEntryProviderBuilder( fun provideTopicEntryProviderBuilder(
backStack: NiaBackStack, backStack: NiaBackStack,
): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = { ): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<TopicRoute> { key -> entry<TopicRoute>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
val id = key.id val id = key.id
TopicScreen( TopicScreen(
showBackButton = true, showBackButton = true,

@ -10,6 +10,7 @@ androidxBrowser = "1.8.0"
androidxComposeBom = "2025.02.00" androidxComposeBom = "2025.02.00"
androidxComposeFoundation = "1.8.0-alpha07" androidxComposeFoundation = "1.8.0-alpha07"
androidxComposeMaterial3Adaptive = "1.1.0-rc01" androidxComposeMaterial3Adaptive = "1.1.0-rc01"
androidxComposeMaterial3AdaptiveNavigation3 = "1.0.0-SNAPSHOT"
androidxComposeRuntimeTracing = "1.7.6" androidxComposeRuntimeTracing = "1.7.6"
androidxCore = "1.15.0" androidxCore = "1.15.0"
androidxCoreSplashscreen = "1.0.1" androidxCoreSplashscreen = "1.0.1"
@ -18,10 +19,11 @@ androidxEspresso = "3.6.1"
androidxHiltNavigationCompose = "1.2.0" androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.7" androidxLifecycle = "2.8.7"
androidxLintGradle = "1.0.0-alpha03" androidxLintGradle = "1.0.0-alpha03"
androidxLifecycleViewModelNavigation3 = "1.0.0-alpha03"
androidxMacroBenchmark = "1.3.4" androidxMacroBenchmark = "1.3.4"
androidxMetrics = "1.0.0-beta01" androidxMetrics = "1.0.0-beta01"
androidxNavigation = "2.8.5" androidxNavigation = "2.8.5"
androidxNavigation3 = "1.0.0-alpha03" androidxNavigation3 = "1.0.0-alpha05"
androidxProfileinstaller = "1.4.1" androidxProfileinstaller = "1.4.1"
androidxTestCore = "1.7.0-rc01" androidxTestCore = "1.7.0-rc01"
androidxTestExt = "1.3.0-rc01" androidxTestExt = "1.3.0-rc01"
@ -80,6 +82,7 @@ androidx-compose-material3-navigationSuite = { group = "androidx.compose.materia
androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidxComposeMaterial3Adaptive" }
androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" }
androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" }
androidx-compose-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3",version.ref="androidxComposeMaterial3AdaptiveNavigation3" }
androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" } androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" } androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" }
@ -96,6 +99,7 @@ androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navig
androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewModel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "androidxLifecycleViewModelNavigation3" }
androidx-lint-gradle = { group = "androidx.lint", name = "lint-gradle", version.ref = "androidxLintGradle" } androidx-lint-gradle = { group = "androidx.lint", name = "lint-gradle", version.ref = "androidxLintGradle" }
androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" } androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }

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

Loading…
Cancel
Save