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.data)
implementation(projects.core.model)
implementation(projects.core.navigation)
implementation(projects.core.analytics)
implementation(projects.core.navigation)
implementation(projects.sync.work)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.adaptive.navigation3)
implementation(libs.androidx.compose.material3.windowSizeClass)
implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.lifecycle.viewModel.navigation3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.profileinstaller)
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.ui.LocalTimeZone
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.rememberNiaAppState
import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme
@ -80,7 +81,7 @@ class MainActivity : ComponentActivity() {
lateinit var niaBackStack: NiaBackStack
@Inject
lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit>
lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()

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

@ -16,23 +16,36 @@
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.compositionLocalOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.NavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
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.NiaBackStackKey
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun NiaNavDisplay(
niaBackStack: NiaBackStack,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit>,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>,
) {
val listDetailStrategy = rememberListDetailSceneStrategy<NiaBackStackKey>()
NavDisplay(
backStack = niaBackStack.backStack,
sceneStrategy = listDetailStrategy,
onBack = { niaBackStack.removeLast() },
entryDecorators = listOf(
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = entryProvider {
entryProviderBuilders.forEach { builder ->
builder()

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

@ -20,8 +20,8 @@ import androidx.annotation.StringRes
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.core.navigation.NiaBackStackKey
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.interests.api.navigation.InterestsRoute
import kotlin.reflect.KClass
@ -49,8 +49,7 @@ enum class TopLevelDestination(
@StringRes val iconTextId: Int,
@StringRes val titleTextId: Int,
val route: KClass<*>,
val baseRoute: KClass<*> = route,
val key: Any
val key: NiaBackStackKey
) {
FOR_YOU(
selectedIcon = NiaIcons.Upcoming,
@ -58,8 +57,7 @@ enum class TopLevelDestination(
iconTextId = forYouR.string.feature_foryou_api_title,
titleTextId = R.string.app_name,
route = ForYouRoute::class,
baseRoute = ForYouBaseRoute::class,
key = ForYouBaseRoute
key = ForYouRoute
),
BOOKMARKS(
selectedIcon = NiaIcons.Bookmarks,
@ -78,3 +76,5 @@ enum class TopLevelDestination(
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.theme.GradientColors
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.search.api.navigation.navigateToSearch
import com.google.samples.apps.nowinandroid.feature.settings.api.SettingsDialog
import com.google.samples.apps.nowinandroid.navigation.NiaNavDisplay
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@ -81,7 +83,7 @@ import com.google.samples.apps.nowinandroid.feature.settings.api.R as settingsR
@Composable
fun NiaApp(
appState: NiaAppState,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit>,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>,
modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) {
@ -132,7 +134,7 @@ fun NiaApp(
)
internal fun NiaApp(
appState: NiaAppState,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<Any>.() -> Unit>,
entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaBackStackKey>.() -> Unit>,
showSettingsDialog: Boolean,
onSettingsDismissed: () -> Unit,
onTopAppBarActionClick: () -> Unit,
@ -141,7 +143,7 @@ internal fun NiaApp(
) {
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources
.collectAsStateWithLifecycle()
val currentTopLevelKey = appState.currentTopLevelDestination
val currentTopLevelKey = appState.currentTopLevelDestination!!.key
if (showSettingsDialog) {
@ -156,10 +158,7 @@ internal fun NiaApp(
navigationSuiteItems = {
appState.topLevelDestinations.forEach { destination ->
val hasUnread = unreadDestinations.contains(destination)
// val selected = currentDestination
// .isRouteInHierarchy(destination.baseRoute)
val selected = destination.key == currentTopLevelKey
println("cfok destination:$destination, currentDest:$currentTopLevelKey")
item(
selected = selected,
onClick = { appState.navigateToTopLevelDestination(destination) },
@ -233,7 +232,7 @@ internal fun NiaApp(
containerColor = Color.Transparent,
),
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.FOR_YOU
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestinations
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -98,12 +99,7 @@ class NiaAppState(
// }
val currentTopLevelDestination: TopLevelDestination?
@Composable get() {
return TopLevelDestination.entries.firstOrNull { topLevelDestination ->
topLevelDestination.key == niaBackStack.currentTopLevelKey
// currentDestination?.hasRoute(route = topLevelDestination.route) == true
}
}
@Composable get() = TopLevelDestinations[niaBackStack.currentTopLevelKey]
val isOffline = networkMonitor.isOnline
.map(Boolean::not)
@ -178,9 +174,6 @@ class NiaAppState(
}
fun navigateToSearch() = navController.navigateToSearch()
fun navigateToSearchNav3() = niaBackStack.navigateToSearch(
onInterestsClick = { navigateToTopLevelDestination(INTERESTS) }
)
}
/**

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

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

@ -21,3 +21,7 @@ plugins {
android {
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.NavGraphBuilder
import androidx.navigation.NavOptions
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import kotlinx.serialization.Serializable
@Serializable object BookmarksRoute
@Serializable object BookmarksRoute: NiaBackStackKey
fun NavController.navigateToBookmarks(navOptions: NavOptions) =
navigate(route = BookmarksRoute, navOptions)

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

@ -35,6 +35,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner
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 org.junit.Rule
import org.junit.Test
@ -63,7 +64,7 @@ class BookmarksScreenTest {
composeTestRule
.onNodeWithContentDescription(
composeTestRule.activity.resources.getString(R.string.feature_bookmarks_api_loading),
composeTestRule.activity.resources.getString(R.string.feature_bookmarks_impl_loading),
)
.assertExists()
}
@ -160,13 +161,13 @@ class BookmarksScreenTest {
composeTestRule
.onNodeWithText(
composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_error),
composeTestRule.activity.getString(R.string.feature_bookmarks_impl_empty_error),
)
.assertExists()
composeTestRule
.onNodeWithText(
composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_description),
composeTestRule.activity.getString(R.string.feature_bookmarks_impl_empty_description),
)
.assertExists()
}

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

@ -23,8 +23,9 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.navigation3.runtime.EntryProviderBuilder
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.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 dagger.Module
import dagger.Provides
@ -40,10 +41,10 @@ object BookmarksModule {
@IntoSet
fun provideBookmarksEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = {
): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<BookmarksRoute> {
val snackbarHostState = LocalSnackbarHostState.current
BookmarksRoute(
BookmarksScreen(
onTopicClick = backStack::navigateToTopic,
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
@ -58,5 +59,5 @@ object BookmarksModule {
}
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 {
implementation(libs.accompanist.permissions)
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)
api(projects.core.navigation)
}

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

@ -29,7 +29,6 @@ dependencies {
implementation(projects.core.data)
implementation(projects.core.domain)
implementation(projects.core.notifications)
implementation(projects.core.navigation)
implementation(projects.feature.foryou.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.entry
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.topic.api.navigation.navigateToTopic
import dagger.Module
@ -36,8 +37,8 @@ object ForYouModule {
@IntoSet
fun provideForYouEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = {
entry<ForYouBaseRoute> {
): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<ForYouRoute> {
ForYouScreen(
onTopicClick = backStack::navigateToTopic
)

@ -23,5 +23,5 @@ android {
}
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.NavOptions
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
import kotlinx.serialization.Serializable
@Serializable data class InterestsRoute(
// The ID of the topic which will be initially selected at this destination
val initialTopicId: String? = null,
)
): NiaBackStackKey
fun NavController.navigateToInterests(
initialTopicId: String? = null,
@ -37,4 +38,4 @@ fun NiaBackStack.navigateToInterests(
initialTopicId: String? = null,
) {
navigate(InterestsRoute(initialTopicId))
}
}

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

@ -16,11 +16,15 @@
package com.google.samples.apps.nowinandroid.feature.interests.impl
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.navigation.compose.composable
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.navigation3.runtime.EntryProviderBuilder
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.topic.api.TopicDetailPlaceholder
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -31,11 +35,22 @@ import dagger.multibindings.IntoSet
@InstallIn(ActivityComponent::class)
object InterestsModule {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Provides
@IntoSet
fun provideInterestsEntryProviderBuilder(): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = {
entry<InterestsRoute> { key ->
InterestsListDetailScreen()
fun provideInterestsEntryProviderBuilder(
backStack: NiaBackStack
): 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");
* you may not use this file except in compliance with the License.
@ -165,7 +165,7 @@ internal fun InterestsListDetailScreen(
}
},
) {
InterestsRoute(
InterestsScreen(
onTopicClick = ::onTopicClickShowDetailPane,
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
@Composable
fun InterestsRoute(
fun InterestsScreen(
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
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.designsystem.theme.NiaTheme
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 dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
@ -86,7 +87,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen()
InterestsListDetailScreen()
}
}
@ -101,7 +102,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen()
InterestsListDetailScreen()
}
}
@ -116,7 +117,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen()
InterestsListDetailScreen()
}
}
@ -135,7 +136,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen()
InterestsListDetailScreen()
}
}
@ -160,7 +161,7 @@ class InterestsListDetailScreenTest {
BackHandler {
unhandledBackPress = true
}
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen()
InterestsListDetailScreen()
}
}
@ -180,7 +181,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
NiaTheme {
com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsListDetailScreen()
InterestsListDetailScreen()
}
}

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

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

@ -27,9 +27,9 @@ android {
dependencies {
implementation(projects.core.data)
implementation(projects.core.domain)
implementation(projects.core.navigation)
implementation(projects.feature.interests.api)
implementation(projects.feature.search.api)
implementation(projects.feature.topic.api)
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.TrackScreenViewEvent
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
@Composable
internal fun SearchRoute(
internal fun SearchScreen(
onBackClick: () -> Unit,
onInterestsClick: () -> Unit,
onTopicClick: (String) -> Unit,
@ -131,7 +127,7 @@ internal fun SearchRoute(
internal fun SearchScreen(
modifier: Modifier = Modifier,
searchQuery: String = "",
recentSearchesUiState: RecentSearchQueriesUiState = Loading,
recentSearchesUiState: RecentSearchQueriesUiState = RecentSearchQueriesUiState.Loading,
searchResultUiState: SearchResultUiState = SearchResultUiState.Loading,
onSearchQueryChanged: (String) -> Unit = {},
onSearchTriggered: (String) -> Unit = {},
@ -154,11 +150,11 @@ internal fun SearchScreen(
)
when (searchResultUiState) {
SearchResultUiState.Loading,
LoadFailed,
SearchResultUiState.LoadFailed,
-> Unit
SearchNotReady -> SearchNotReadyBody()
EmptyQuery,
SearchResultUiState.SearchNotReady -> SearchNotReadyBody()
SearchResultUiState.EmptyQuery,
-> {
if (recentSearchesUiState is RecentSearchQueriesUiState.Success) {
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.ui.PreviewParameterData.newsResources
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)
@ -30,7 +29,7 @@ import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiSt
*/
class SearchUiStatePreviewParameterProvider : PreviewParameterProvider<SearchResultUiState> {
override val values: Sequence<SearchResultUiState> = sequenceOf(
Success(
SearchResultUiState.Success(
topics = topics.mapIndexed { i, topic ->
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.GetSearchContentsUseCase
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 kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -61,29 +56,29 @@ class SearchViewModel @Inject constructor(
searchContentsRepository.getSearchContentsCount()
.flatMapLatest { totalCount ->
if (totalCount < SEARCH_MIN_FTS_ENTITY_COUNT) {
flowOf(SearchNotReady)
flowOf(SearchResultUiState.SearchNotReady)
} else {
searchQuery.flatMapLatest { query ->
if (query.trim().length < SEARCH_QUERY_MIN_LENGTH) {
flowOf(EmptyQuery)
flowOf(SearchResultUiState.EmptyQuery)
} else {
getSearchContentsUseCase(query)
// Not using .asResult() here, because it emits Loading state every
// time the user types a letter in the search box, which flickers the screen.
.map<UserSearchResult, SearchResultUiState> { data ->
Success(
SearchResultUiState.Success(
topics = data.topics,
newsResources = data.newsResources,
)
}
.catch { emit(LoadFailed) }
.catch { emit(SearchResultUiState.LoadFailed) }
}
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading,
initialValue = SearchResultUiState.Loading,
)
val recentSearchQueriesUiState: StateFlow<RecentSearchQueriesUiState> =

@ -16,13 +16,14 @@
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.entry
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.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.Provides
import dagger.hilt.InstallIn
@ -37,12 +38,12 @@ object SearchModule {
@IntoSet
fun provideSearchEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<Any>.() -> @JvmSuppressWildcards Unit = {
): EntryProviderBuilder<NiaBackStackKey>.() -> Unit = {
entry<SearchRouteNav3> { key ->
SearchRoute(
SearchScreen(
onBackClick = backStack::removeLast,
onInterestsClick = key.onInterestsClick,
onTopicClick = backStack::navigateToInterests,
onInterestsClick = { backStack.navigateToTopLevelDestination(InterestsRoute()) },
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");
* you may not use this file except in compliance with the License.
@ -102,7 +102,7 @@ class SearchViewModelTest {
searchContentsRepository.addTopics(topicsTestData)
val result = viewModel.searchResultUiState.value
assertIs<com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.Success>(result)
assertIs<SearchResultUiState.Success>(result)
}
@Test

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

@ -23,11 +23,12 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
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.TopicScreen
import kotlinx.serialization.Serializable
@Serializable data class TopicRoute(val id: String)
@Serializable data class TopicRoute(val id: String): NiaBackStackKey
fun NiaBackStack.navigateToTopic(
topicId: String,

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 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.
Copyright 2024 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"

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

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

@ -10,6 +10,7 @@ androidxBrowser = "1.8.0"
androidxComposeBom = "2025.02.00"
androidxComposeFoundation = "1.8.0-alpha07"
androidxComposeMaterial3Adaptive = "1.1.0-rc01"
androidxComposeMaterial3AdaptiveNavigation3 = "1.0.0-SNAPSHOT"
androidxComposeRuntimeTracing = "1.7.6"
androidxCore = "1.15.0"
androidxCoreSplashscreen = "1.0.1"
@ -18,10 +19,11 @@ androidxEspresso = "3.6.1"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.7"
androidxLintGradle = "1.0.0-alpha03"
androidxLifecycleViewModelNavigation3 = "1.0.0-alpha03"
androidxMacroBenchmark = "1.3.4"
androidxMetrics = "1.0.0-beta01"
androidxNavigation = "2.8.5"
androidxNavigation3 = "1.0.0-alpha03"
androidxNavigation3 = "1.0.0-alpha05"
androidxProfileinstaller = "1.4.1"
androidxTestCore = "1.7.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-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-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-runtime = { group = "androidx.compose.runtime", name = "runtime" }
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-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-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-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }

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

Loading…
Cancel
Save