Merge branch 'android:main' into rename-test-function

pull/1258/head
Jaehwa Noh 9 months ago committed by GitHub
commit d67b1a2504
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -12,41 +12,41 @@ androidx.browser:browser:1.6.0
androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-jvm:1.4.0
androidx.collection:collection-ktx:1.4.0 androidx.collection:collection-ktx:1.4.0
androidx.collection:collection:1.4.0 androidx.collection:collection:1.4.0
androidx.compose.animation:animation-android:1.6.1 androidx.compose.animation:animation-android:1.6.2
androidx.compose.animation:animation-core-android:1.6.1 androidx.compose.animation:animation-core-android:1.6.2
androidx.compose.animation:animation-core:1.6.1 androidx.compose.animation:animation-core:1.6.2
androidx.compose.animation:animation:1.6.1 androidx.compose.animation:animation:1.6.2
androidx.compose.foundation:foundation-android:1.6.1 androidx.compose.foundation:foundation-android:1.6.2
androidx.compose.foundation:foundation-layout-android:1.6.1 androidx.compose.foundation:foundation-layout-android:1.6.2
androidx.compose.foundation:foundation-layout:1.6.1 androidx.compose.foundation:foundation-layout:1.6.2
androidx.compose.foundation:foundation:1.6.1 androidx.compose.foundation:foundation:1.6.2
androidx.compose.material3:material3-android:1.2.0 androidx.compose.material3:material3-android:1.2.0
androidx.compose.material3:material3:1.2.0 androidx.compose.material3:material3:1.2.0
androidx.compose.material:material-icons-core-android:1.6.1 androidx.compose.material:material-icons-core-android:1.6.2
androidx.compose.material:material-icons-core:1.6.1 androidx.compose.material:material-icons-core:1.6.2
androidx.compose.material:material-icons-extended-android:1.6.1 androidx.compose.material:material-icons-extended-android:1.6.2
androidx.compose.material:material-icons-extended:1.6.1 androidx.compose.material:material-icons-extended:1.6.2
androidx.compose.material:material-ripple-android:1.6.1 androidx.compose.material:material-ripple-android:1.6.2
androidx.compose.material:material-ripple:1.6.1 androidx.compose.material:material-ripple:1.6.2
androidx.compose.runtime:runtime-android:1.6.1 androidx.compose.runtime:runtime-android:1.6.2
androidx.compose.runtime:runtime-saveable-android:1.6.1 androidx.compose.runtime:runtime-saveable-android:1.6.2
androidx.compose.runtime:runtime-saveable:1.6.1 androidx.compose.runtime:runtime-saveable:1.6.2
androidx.compose.runtime:runtime:1.6.1 androidx.compose.runtime:runtime:1.6.2
androidx.compose.ui:ui-android:1.6.1 androidx.compose.ui:ui-android:1.6.2
androidx.compose.ui:ui-geometry-android:1.6.1 androidx.compose.ui:ui-geometry-android:1.6.2
androidx.compose.ui:ui-geometry:1.6.1 androidx.compose.ui:ui-geometry:1.6.2
androidx.compose.ui:ui-graphics-android:1.6.1 androidx.compose.ui:ui-graphics-android:1.6.2
androidx.compose.ui:ui-graphics:1.6.1 androidx.compose.ui:ui-graphics:1.6.2
androidx.compose.ui:ui-text-android:1.6.1 androidx.compose.ui:ui-text-android:1.6.2
androidx.compose.ui:ui-text:1.6.1 androidx.compose.ui:ui-text:1.6.2
androidx.compose.ui:ui-tooling-preview-android:1.6.1 androidx.compose.ui:ui-tooling-preview-android:1.6.2
androidx.compose.ui:ui-tooling-preview:1.6.1 androidx.compose.ui:ui-tooling-preview:1.6.2
androidx.compose.ui:ui-unit-android:1.6.1 androidx.compose.ui:ui-unit-android:1.6.2
androidx.compose.ui:ui-unit:1.6.1 androidx.compose.ui:ui-unit:1.6.2
androidx.compose.ui:ui-util-android:1.6.1 androidx.compose.ui:ui-util-android:1.6.2
androidx.compose.ui:ui-util:1.6.1 androidx.compose.ui:ui-util:1.6.2
androidx.compose.ui:ui:1.6.1 androidx.compose.ui:ui:1.6.2
androidx.compose:compose-bom:2024.02.00 androidx.compose:compose-bom:2024.02.01
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.12.0 androidx.core:core-ktx:1.12.0
androidx.core:core:1.12.0 androidx.core:core:1.12.0

@ -89,17 +89,21 @@ dependencies {
implementation(projects.sync.work) implementation(projects.sync.work)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.windowSizeClass)
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.tracing.ktx) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.compose.material3.windowSizeClass)
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.kotlinx.coroutines.guava) implementation(libs.kotlinx.coroutines.guava)
implementation(libs.coil.kt) implementation(libs.coil.kt)
ksp(libs.hilt.compiler)
debugImplementation(libs.androidx.compose.ui.testManifest) debugImplementation(libs.androidx.compose.ui.testManifest)
debugImplementation(projects.uiTestHiltManifest) debugImplementation(projects.uiTestHiltManifest)

@ -13,44 +13,46 @@ androidx.browser:browser:1.6.0
androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-jvm:1.4.0
androidx.collection:collection-ktx:1.4.0 androidx.collection:collection-ktx:1.4.0
androidx.collection:collection:1.4.0 androidx.collection:collection:1.4.0
androidx.compose.animation:animation-android:1.6.1 androidx.compose.animation:animation-android:1.6.2
androidx.compose.animation:animation-core-android:1.6.1 androidx.compose.animation:animation-core-android:1.6.2
androidx.compose.animation:animation-core:1.6.1 androidx.compose.animation:animation-core:1.6.2
androidx.compose.animation:animation:1.6.1 androidx.compose.animation:animation:1.6.2
androidx.compose.foundation:foundation-android:1.6.1 androidx.compose.foundation:foundation-android:1.6.2
androidx.compose.foundation:foundation-layout-android:1.6.1 androidx.compose.foundation:foundation-layout-android:1.6.2
androidx.compose.foundation:foundation-layout:1.6.1 androidx.compose.foundation:foundation-layout:1.6.2
androidx.compose.foundation:foundation:1.6.1 androidx.compose.foundation:foundation:1.6.2
androidx.compose.material3:material3-adaptive-android:1.0.0-alpha06
androidx.compose.material3:material3-adaptive:1.0.0-alpha06
androidx.compose.material3:material3-android:1.2.0 androidx.compose.material3:material3-android:1.2.0
androidx.compose.material3:material3-window-size-class-android:1.2.0 androidx.compose.material3:material3-window-size-class-android:1.2.0
androidx.compose.material3:material3-window-size-class:1.2.0 androidx.compose.material3:material3-window-size-class:1.2.0
androidx.compose.material3:material3:1.2.0 androidx.compose.material3:material3:1.2.0
androidx.compose.material:material-icons-core-android:1.6.1 androidx.compose.material:material-icons-core-android:1.6.2
androidx.compose.material:material-icons-core:1.6.1 androidx.compose.material:material-icons-core:1.6.2
androidx.compose.material:material-icons-extended-android:1.6.1 androidx.compose.material:material-icons-extended-android:1.6.2
androidx.compose.material:material-icons-extended:1.6.1 androidx.compose.material:material-icons-extended:1.6.2
androidx.compose.material:material-ripple-android:1.6.1 androidx.compose.material:material-ripple-android:1.6.2
androidx.compose.material:material-ripple:1.6.1 androidx.compose.material:material-ripple:1.6.2
androidx.compose.runtime:runtime-android:1.6.1 androidx.compose.runtime:runtime-android:1.6.2
androidx.compose.runtime:runtime-saveable-android:1.6.1 androidx.compose.runtime:runtime-saveable-android:1.6.2
androidx.compose.runtime:runtime-saveable:1.6.1 androidx.compose.runtime:runtime-saveable:1.6.2
androidx.compose.runtime:runtime-tracing:1.0.0-beta01 androidx.compose.runtime:runtime-tracing:1.0.0-beta01
androidx.compose.runtime:runtime:1.6.1 androidx.compose.runtime:runtime:1.6.2
androidx.compose.ui:ui-android:1.6.1 androidx.compose.ui:ui-android:1.6.2
androidx.compose.ui:ui-geometry-android:1.6.1 androidx.compose.ui:ui-geometry-android:1.6.2
androidx.compose.ui:ui-geometry:1.6.1 androidx.compose.ui:ui-geometry:1.6.2
androidx.compose.ui:ui-graphics-android:1.6.1 androidx.compose.ui:ui-graphics-android:1.6.2
androidx.compose.ui:ui-graphics:1.6.1 androidx.compose.ui:ui-graphics:1.6.2
androidx.compose.ui:ui-text-android:1.6.1 androidx.compose.ui:ui-text-android:1.6.2
androidx.compose.ui:ui-text:1.6.1 androidx.compose.ui:ui-text:1.6.2
androidx.compose.ui:ui-tooling-preview-android:1.6.1 androidx.compose.ui:ui-tooling-preview-android:1.6.2
androidx.compose.ui:ui-tooling-preview:1.6.1 androidx.compose.ui:ui-tooling-preview:1.6.2
androidx.compose.ui:ui-unit-android:1.6.1 androidx.compose.ui:ui-unit-android:1.6.2
androidx.compose.ui:ui-unit:1.6.1 androidx.compose.ui:ui-unit:1.6.2
androidx.compose.ui:ui-util-android:1.6.1 androidx.compose.ui:ui-util-android:1.6.2
androidx.compose.ui:ui-util:1.6.1 androidx.compose.ui:ui-util:1.6.2
androidx.compose.ui:ui:1.6.1 androidx.compose.ui:ui:1.6.2
androidx.compose:compose-bom:2024.02.00 androidx.compose:compose-bom:2024.02.01
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.12.0 androidx.core:core-ktx:1.12.0
androidx.core:core-splashscreen:1.0.1 androidx.core:core-splashscreen:1.0.1
@ -116,7 +118,8 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager:viewpager:1.0.0 androidx.viewpager:viewpager:1.0.0
androidx.window:window:1.0.0 androidx.window.extensions.core:core:1.0.0
androidx.window:window:1.2.0
androidx.work:work-runtime-ktx:2.9.0 androidx.work:work-runtime-ktx:2.9.0
androidx.work:work-runtime:2.9.0 androidx.work:work-runtime:2.9.0
com.caverock:androidsvg-aar:1.4 com.caverock:androidsvg-aar:1.4

@ -274,10 +274,10 @@ class NavigationTest {
// Select the last topic // Select the last topic
val topic = runBlocking { val topic = runBlocking {
topicsRepository.getTopics().first().sortedBy(Topic::name).last().name topicsRepository.getTopics().first().sortedBy(Topic::name).last()
} }
onNodeWithTag("interests:topics").performScrollToNode(hasText(topic)) onNodeWithTag("interests:topics").performScrollToNode(hasText(topic.name))
onNodeWithText(topic).performClick() onNodeWithText(topic.name).performClick()
// Switch tab // Switch tab
onNodeWithText(forYou).performClick() onNodeWithText(forYou).performClick()
@ -285,8 +285,8 @@ class NavigationTest {
// Come back to Interests // Come back to Interests
onNodeWithText(interests).performClick() onNodeWithText(interests).performClick()
// Verify we're not in the list of interests // Verify the topic is still shown
onNodeWithTag("interests:topics").assertDoesNotExist() onNodeWithTag("topic:${topic.id}").assertExists()
} }
} }
} }

@ -22,12 +22,11 @@ import androidx.navigation.compose.NavHost
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import com.google.samples.apps.nowinandroid.ui.NiaAppState import com.google.samples.apps.nowinandroid.ui.NiaAppState
import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen
/** /**
* Top-level navigation graph. Navigation is organized as explained at * Top-level navigation graph. Navigation is organized as explained at
@ -49,24 +48,16 @@ fun NiaNavHost(
startDestination = startDestination, startDestination = startDestination,
modifier = modifier, modifier = modifier,
) { ) {
forYouScreen(onTopicClick = navController::navigateToTopic) forYouScreen(onTopicClick = navController::navigateToInterests)
bookmarksScreen( bookmarksScreen(
onTopicClick = navController::navigateToTopic, onTopicClick = navController::navigateToInterests,
onShowSnackbar = onShowSnackbar, onShowSnackbar = onShowSnackbar,
) )
searchScreen( searchScreen(
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) }, onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
onTopicClick = navController::navigateToTopic, onTopicClick = navController::navigateToInterests,
)
interestsGraph(
onTopicClick = navController::navigateToTopic,
nestedGraphs = {
topicScreen(
onBackClick = navController::popBackStack,
onTopicClick = navController::navigateToTopic,
)
},
) )
interestsListDetailScreen()
} }
} }

@ -39,7 +39,7 @@ import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigat
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou
import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterestsGraph import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination 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
@ -173,7 +173,7 @@ class NiaAppState(
when (topLevelDestination) { when (topLevelDestination) {
FOR_YOU -> navController.navigateToForYou(topLevelNavOptions) FOR_YOU -> navController.navigateToForYou(topLevelNavOptions)
BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions) BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions)
INTERESTS -> navController.navigateToInterestsGraph(topLevelNavOptions) INTERESTS -> navController.navigateToInterests(null, topLevelNavOptions)
} }
} }
} }

@ -0,0 +1,35 @@
/*
* 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.
*/
package com.google.samples.apps.nowinandroid.ui.interests2pane
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@HiltViewModel
class Interests2PaneViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
val selectedTopicId: StateFlow<String?> = savedStateHandle.getStateFlow(TOPIC_ID_ARG, null)
fun onTopicClick(topicId: String?) {
savedStateHandle[TOPIC_ID_ARG] = topicId
}
}

@ -0,0 +1,136 @@
/*
* 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.
*/
package com.google.samples.apps.nowinandroid.ui.interests2pane
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.PaneAdaptedValue
import androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE
import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TOPIC_ROUTE
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
private const val DETAIL_PANE_NAVHOST_ROUTE = "detail_pane_route"
fun NavGraphBuilder.interestsListDetailScreen() {
composable(
route = INTERESTS_ROUTE,
arguments = listOf(
navArgument(TOPIC_ID_ARG) {
type = NavType.StringType
defaultValue = null
nullable = true
},
),
) {
InterestsListDetailScreen()
}
}
@Composable
internal fun InterestsListDetailScreen(
viewModel: Interests2PaneViewModel = hiltViewModel(),
) {
val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()
InterestsListDetailScreen(
selectedTopicId = selectedTopicId,
onTopicClick = viewModel::onTopicClick,
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
internal fun InterestsListDetailScreen(
selectedTopicId: String?,
onTopicClick: (String) -> Unit,
) {
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator<Nothing>()
BackHandler(listDetailNavigator.canNavigateBack()) {
listDetailNavigator.navigateBack()
}
val nestedNavController = rememberNavController()
fun onTopicClickShowDetailPane(topicId: String) {
onTopicClick(topicId)
nestedNavController.navigateToTopic(topicId) {
popUpTo(DETAIL_PANE_NAVHOST_ROUTE)
}
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
}
ListDetailPaneScaffold(
scaffoldState = listDetailNavigator.scaffoldState,
listPane = {
InterestsRoute(
onTopicClick = ::onTopicClickShowDetailPane,
highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
)
},
detailPane = {
NavHost(
navController = nestedNavController,
startDestination = TOPIC_ROUTE,
route = DETAIL_PANE_NAVHOST_ROUTE,
) {
topicScreen(
showBackButton = !listDetailNavigator.isListPaneVisible(),
onBackClick = listDetailNavigator::navigateBack,
onTopicClick = ::onTopicClickShowDetailPane,
)
composable(route = TOPIC_ROUTE) {
Box {
Text("Placeholder")
}
}
}
},
)
LaunchedEffect(Unit) {
if (selectedTopicId != null) {
// Initial topic ID was provided when navigating to Interests, so show its details.
onTopicClickShowDetailPane(selectedTopicId)
}
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun <T> ThreePaneScaffoldNavigator<T>.isListPaneVisible(): Boolean =
scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun <T> ThreePaneScaffoldNavigator<T>.isDetailPaneVisible(): Boolean =
scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded

@ -39,6 +39,8 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
extensions.configure<ApplicationExtension> { extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 34
@Suppress("UnstableApiUsage")
testOptions.animationsDisabled = true
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
} }
extensions.configure<ApplicationAndroidComponentsExtension> { extensions.configure<ApplicationAndroidComponentsExtension> {

@ -34,6 +34,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
testInstrumentationRunner = testInstrumentationRunner =
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
} }
testOptions.animationsDisabled = true
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
} }

@ -40,6 +40,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
extensions.configure<LibraryExtension> { extensions.configure<LibraryExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 34
testOptions.animationsDisabled = true
configureFlavors(this) configureFlavors(this)
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
// The resource prefix is derived from the module name, // The resource prefix is derived from the module name,

@ -27,6 +27,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -48,6 +49,9 @@ class NewsResourceDaoTest {
topicDao = db.topicDao() topicDao = db.topicDao()
} }
@After
fun closeDb() = db.close()
@Test @Test
fun newsResourceDao_fetches_items_by_descending_publish_date() = runTest { fun newsResourceDao_fetches_items_by_descending_publish_date() = runTest {
val newsResourceEntities = listOf( val newsResourceEntities = listOf(

@ -74,7 +74,10 @@ class InterestsScreenTest {
fun interestsWithTopics_whenTopicsFollowed_showFollowedAndUnfollowedTopicsWithInfo() { fun interestsWithTopics_whenTopicsFollowed_showFollowedAndUnfollowedTopicsWithInfo() {
composeTestRule.setContent { composeTestRule.setContent {
InterestsScreen( InterestsScreen(
uiState = InterestsUiState.Interests(topics = followableTopicTestData), uiState = InterestsUiState.Interests(
topics = followableTopicTestData,
selectedTopicId = null,
),
) )
} }

@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -49,6 +50,7 @@ fun InterestsItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
iconModifier: Modifier = Modifier, iconModifier: Modifier = Modifier,
description: String = "", description: String = "",
isSelected: Boolean = false,
) { ) {
ListItem( ListItem(
leadingContent = { leadingContent = {
@ -83,10 +85,16 @@ fun InterestsItem(
) )
}, },
colors = ListItemDefaults.colors( colors = ListItemDefaults.colors(
containerColor = Color.Transparent, containerColor = if (isSelected) {
MaterialTheme.colorScheme.surfaceVariant
} else {
Color.Transparent
},
), ),
modifier = modifier modifier = modifier
.semantics(mergeDescendants = true) { /* no-op */ } .semantics(mergeDescendants = true) {
selected = isSelected
}
.clickable(enabled = true, onClick = onClick), .clickable(enabled = true, onClick = onClick),
) )
} }
@ -179,3 +187,21 @@ private fun InterestsCardWithEmptyDescriptionPreview() {
} }
} }
} }
@Preview
@Composable
private fun InterestsCardSelectedPreview() {
NiaTheme {
Surface {
InterestsItem(
name = "Compose",
description = "",
following = true,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { },
isSelected = true,
)
}
}
}

@ -35,9 +35,10 @@ import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParame
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
@Composable @Composable
internal fun InterestsRoute( fun InterestsRoute(
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
highlightSelectedTopic: Boolean = false,
viewModel: InterestsViewModel = hiltViewModel(), viewModel: InterestsViewModel = hiltViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -45,7 +46,11 @@ internal fun InterestsRoute(
InterestsScreen( InterestsScreen(
uiState = uiState, uiState = uiState,
followTopic = viewModel::followTopic, followTopic = viewModel::followTopic,
onTopicClick = onTopicClick, onTopicClick = {
viewModel.onTopicClick(it)
onTopicClick(it)
},
highlightSelectedTopic = highlightSelectedTopic,
modifier = modifier, modifier = modifier,
) )
} }
@ -56,6 +61,7 @@ internal fun InterestsScreen(
followTopic: (String, Boolean) -> Unit, followTopic: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
highlightSelectedTopic: Boolean = false,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
@ -67,13 +73,17 @@ internal fun InterestsScreen(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = R.string.feature_interests_loading), contentDesc = stringResource(id = R.string.feature_interests_loading),
) )
is InterestsUiState.Interests -> is InterestsUiState.Interests ->
TopicsTabContent( TopicsTabContent(
topics = uiState.topics, topics = uiState.topics,
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
onFollowButtonClick = followTopic, onFollowButtonClick = followTopic,
selectedTopicId = uiState.selectedTopicId,
highlightSelectedTopic = highlightSelectedTopic,
modifier = modifier, modifier = modifier,
) )
is InterestsUiState.Empty -> InterestsEmptyScreen() is InterestsUiState.Empty -> InterestsEmptyScreen()
} }
} }
@ -95,6 +105,7 @@ fun InterestsScreenPopulated(
NiaBackground { NiaBackground {
InterestsScreen( InterestsScreen(
uiState = InterestsUiState.Interests( uiState = InterestsUiState.Interests(
selectedTopicId = null,
topics = followableTopics, topics = followableTopics,
), ),
followTopic = { _, _ -> }, followTopic = { _, _ -> },

@ -16,46 +16,57 @@
package com.google.samples.apps.nowinandroid.feature.interests package com.google.samples.apps.nowinandroid.feature.interests
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField import com.google.samples.apps.nowinandroid.core.domain.TopicSortField
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG
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
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class InterestsViewModel @Inject constructor( class InterestsViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
val userDataRepository: UserDataRepository, val userDataRepository: UserDataRepository,
getFollowableTopics: GetFollowableTopicsUseCase, getFollowableTopics: GetFollowableTopicsUseCase,
) : ViewModel() { ) : ViewModel() {
val uiState: StateFlow<InterestsUiState> = val selectedTopicId: StateFlow<String?> = savedStateHandle.getStateFlow(TOPIC_ID_ARG, null)
getFollowableTopics(sortBy = TopicSortField.NAME).map(
InterestsUiState::Interests, val uiState: StateFlow<InterestsUiState> = combine(
).stateIn( selectedTopicId,
scope = viewModelScope, getFollowableTopics(sortBy = TopicSortField.NAME),
started = SharingStarted.WhileSubscribed(5_000), InterestsUiState::Interests,
initialValue = InterestsUiState.Loading, ).stateIn(
) scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading,
)
fun followTopic(followedTopicId: String, followed: Boolean) { fun followTopic(followedTopicId: String, followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
userDataRepository.setTopicIdFollowed(followedTopicId, followed) userDataRepository.setTopicIdFollowed(followedTopicId, followed)
} }
} }
fun onTopicClick(topicId: String?) {
savedStateHandle[TOPIC_ID_ARG] = topicId
}
} }
sealed interface InterestsUiState { sealed interface InterestsUiState {
data object Loading : InterestsUiState data object Loading : InterestsUiState
data class Interests( data class Interests(
val selectedTopicId: String?,
val topics: List<FollowableTopic>, val topics: List<FollowableTopic>,
) : InterestsUiState ) : InterestsUiState

@ -47,6 +47,8 @@ fun TopicsTabContent(
onFollowButtonClick: (String, Boolean) -> Unit, onFollowButtonClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
withBottomSpacer: Boolean = true, withBottomSpacer: Boolean = true,
selectedTopicId: String? = null,
highlightSelectedTopic: Boolean = false,
) { ) {
Box( Box(
modifier = modifier modifier = modifier
@ -63,6 +65,7 @@ fun TopicsTabContent(
topics.forEach { followableTopic -> topics.forEach { followableTopic ->
val topicId = followableTopic.topic.id val topicId = followableTopic.topic.id
item(key = topicId) { item(key = topicId) {
val isSelected = highlightSelectedTopic && topicId == selectedTopicId
InterestsItem( InterestsItem(
name = followableTopic.topic.name, name = followableTopic.topic.name,
following = followableTopic.isFollowed, following = followableTopic.isFollowed,
@ -70,6 +73,7 @@ fun TopicsTabContent(
topicImageUrl = followableTopic.topic.imageUrl, topicImageUrl = followableTopic.topic.imageUrl,
onClick = { onTopicClick(topicId) }, onClick = { onTopicClick(topicId) },
onFollowButtonClick = { onFollowButtonClick(topicId, it) }, onFollowButtonClick = { onFollowButtonClick(topicId, it) },
isSelected = isSelected,
) )
} }
} }

@ -19,26 +19,37 @@ package com.google.samples.apps.nowinandroid.feature.interests.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 androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navigation import androidx.navigation.navArgument
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
private const val INTERESTS_GRAPH_ROUTE_PATTERN = "interests_graph" const val TOPIC_ID_ARG = "topicId"
const val INTERESTS_ROUTE = "interests_route" const val INTERESTS_ROUTE_BASE = "interests_route"
const val INTERESTS_ROUTE = "$INTERESTS_ROUTE_BASE?$TOPIC_ID_ARG={$TOPIC_ID_ARG}"
fun NavController.navigateToInterestsGraph(navOptions: NavOptions) = navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions) fun NavController.navigateToInterests(topicId: String? = null, navOptions: NavOptions? = null) {
val route = if (topicId != null) {
"${INTERESTS_ROUTE_BASE}?${TOPIC_ID_ARG}=$topicId"
} else {
INTERESTS_ROUTE_BASE
}
navigate(route, navOptions)
}
fun NavGraphBuilder.interestsGraph( fun NavGraphBuilder.interestsScreen(
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
nestedGraphs: NavGraphBuilder.() -> Unit,
) { ) {
navigation( composable(
route = INTERESTS_GRAPH_ROUTE_PATTERN, route = INTERESTS_ROUTE,
startDestination = INTERESTS_ROUTE, arguments = listOf(
navArgument(TOPIC_ID_ARG) {
defaultValue = null
nullable = true
type = NavType.StringType
},
),
) { ) {
composable(route = INTERESTS_ROUTE) { InterestsRoute(onTopicClick = onTopicClick)
InterestsRoute(onTopicClick)
}
nestedGraphs()
} }
} }

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.interests package com.google.samples.apps.nowinandroid.interests
import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
@ -24,6 +25,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState
import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel
import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -53,6 +55,7 @@ class InterestsViewModelTest {
@Before @Before
fun setup() { fun setup() {
viewModel = InterestsViewModel( viewModel = InterestsViewModel(
savedStateHandle = SavedStateHandle(mapOf(TOPIC_ID_ARG to testInputTopics[0].topic.id)),
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
getFollowableTopics = getFollowableTopicsUseCase, getFollowableTopics = getFollowableTopicsUseCase,
) )
@ -93,7 +96,10 @@ class InterestsViewModelTest {
) )
assertEquals( assertEquals(
InterestsUiState.Interests(topics = testOutputTopics), InterestsUiState.Interests(
topics = testOutputTopics,
selectedTopicId = testInputTopics[0].topic.id,
),
viewModel.uiState.value, viewModel.uiState.value,
) )
@ -123,7 +129,10 @@ class InterestsViewModelTest {
) )
assertEquals( assertEquals(
InterestsUiState.Interests(topics = testInputTopics), InterestsUiState.Interests(
topics = testInputTopics,
selectedTopicId = testInputTopics[0].topic.id,
),
viewModel.uiState.value, viewModel.uiState.value,
) )

@ -55,6 +55,7 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
showBackButton = true,
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},
@ -75,6 +76,7 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Success(testTopic), topicUiState = TopicUiState.Success(testTopic),
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
showBackButton = true,
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},
@ -100,6 +102,7 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Success(userNewsResourcesTestData), newsUiState = NewsUiState.Success(userNewsResourcesTestData),
showBackButton = true,
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},
@ -123,6 +126,7 @@ class TopicScreenTest {
newsUiState = NewsUiState.Success( newsUiState = NewsUiState.Success(
userNewsResourcesTestData, userNewsResourcesTestData,
), ),
showBackButton = true,
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},

@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.layout.windowInsetsTopHeight
@ -44,6 +45,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -70,6 +72,7 @@ import com.google.samples.apps.nowinandroid.feature.topic.R.string
@Composable @Composable
internal fun TopicRoute( internal fun TopicRoute(
showBackButton: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -82,7 +85,8 @@ internal fun TopicRoute(
TopicScreen( TopicScreen(
topicUiState = topicUiState, topicUiState = topicUiState,
newsUiState = newsUiState, newsUiState = newsUiState,
modifier = modifier, modifier = modifier.testTag("topic:${viewModel.topicId}"),
showBackButton = showBackButton,
onBackClick = onBackClick, onBackClick = onBackClick,
onFollowClick = viewModel::followTopicToggle, onFollowClick = viewModel::followTopicToggle,
onBookmarkChanged = viewModel::bookmarkNews, onBookmarkChanged = viewModel::bookmarkNews,
@ -96,6 +100,7 @@ internal fun TopicRoute(
internal fun TopicScreen( internal fun TopicScreen(
topicUiState: TopicUiState, topicUiState: TopicUiState,
newsUiState: NewsUiState, newsUiState: NewsUiState,
showBackButton: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onFollowClick: (Boolean) -> Unit, onFollowClick: (Boolean) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
@ -127,6 +132,7 @@ internal fun TopicScreen(
is TopicUiState.Success -> { is TopicUiState.Success -> {
item { item {
TopicToolbar( TopicToolbar(
showBackButton = showBackButton,
onBackClick = onBackClick, onBackClick = onBackClick,
onFollowClick = onFollowClick, onFollowClick = onFollowClick,
uiState = topicUiState.followableTopic, uiState = topicUiState.followableTopic,
@ -270,6 +276,7 @@ private fun TopicBodyPreview() {
private fun TopicToolbar( private fun TopicToolbar(
uiState: FollowableTopic, uiState: FollowableTopic,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showBackButton: Boolean = true,
onBackClick: () -> Unit = {}, onBackClick: () -> Unit = {},
onFollowClick: (Boolean) -> Unit = {}, onFollowClick: (Boolean) -> Unit = {},
) { ) {
@ -280,13 +287,18 @@ private fun TopicToolbar(
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 32.dp), .padding(bottom = 32.dp),
) { ) {
IconButton(onClick = { onBackClick() }) { if (showBackButton) {
Icon( IconButton(onClick = { onBackClick() }) {
imageVector = NiaIcons.ArrowBack, Icon(
contentDescription = stringResource( imageVector = NiaIcons.ArrowBack,
id = com.google.samples.apps.nowinandroid.core.ui.R.string.core_ui_back, contentDescription = stringResource(
), id = com.google.samples.apps.nowinandroid.core.ui.R.string.core_ui_back,
) ),
)
}
} else {
// Keeps the NiaFilterChip aligned to the end of the Row.
Spacer(modifier = Modifier.width(1.dp))
} }
val selected = uiState.isFollowed val selected = uiState.isFollowed
NiaFilterChip( NiaFilterChip(
@ -314,6 +326,7 @@ fun TopicScreenPopulated(
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Success(userNewsResources[0].followableTopics[0]), topicUiState = TopicUiState.Success(userNewsResources[0].followableTopics[0]),
newsUiState = NewsUiState.Success(userNewsResources), newsUiState = NewsUiState.Success(userNewsResources),
showBackButton = true,
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
@ -332,6 +345,7 @@ fun TopicScreenLoading() {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
showBackButton = true,
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },

@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
@ -32,20 +33,23 @@ private val URL_CHARACTER_ENCODING = UTF_8.name()
@VisibleForTesting @VisibleForTesting
internal const val TOPIC_ID_ARG = "topicId" internal const val TOPIC_ID_ARG = "topicId"
const val TOPIC_ROUTE = "topic_route"
internal class TopicArgs(val topicId: String) { internal class TopicArgs(val topicId: String) {
constructor(savedStateHandle: SavedStateHandle) : constructor(savedStateHandle: SavedStateHandle) :
this(URLDecoder.decode(checkNotNull(savedStateHandle[TOPIC_ID_ARG]), URL_CHARACTER_ENCODING)) this(URLDecoder.decode(checkNotNull(savedStateHandle[TOPIC_ID_ARG]), URL_CHARACTER_ENCODING))
} }
fun NavController.navigateToTopic(topicId: String) { fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) {
val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING)
navigate("topic_route/$encodedId") { val newRoute = "$TOPIC_ROUTE/$encodedId"
launchSingleTop = true navigate(newRoute) {
navOptions()
} }
} }
fun NavGraphBuilder.topicScreen( fun NavGraphBuilder.topicScreen(
showBackButton: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
) { ) {
@ -55,6 +59,10 @@ fun NavGraphBuilder.topicScreen(
navArgument(TOPIC_ID_ARG) { type = NavType.StringType }, navArgument(TOPIC_ID_ARG) { type = NavType.StringType },
), ),
) { ) {
TopicRoute(onBackClick = onBackClick, onTopicClick = onTopicClick) TopicRoute(
showBackButton = showBackButton,
onBackClick = onBackClick,
onTopicClick = onTopicClick,
)
} }
} }

@ -7,8 +7,9 @@ androidTools = "31.3.0"
androidxActivity = "1.8.0" androidxActivity = "1.8.0"
androidxAppCompat = "1.6.1" androidxAppCompat = "1.6.1"
androidxBrowser = "1.6.0" androidxBrowser = "1.6.0"
androidxComposeBom = "2024.02.00" androidxComposeBom = "2024.02.01"
androidxComposeCompiler = "1.5.8" androidxComposeCompiler = "1.5.8"
androidxComposeMaterial3Adaptive = "1.0.0-alpha06"
androidxComposeRuntimeTracing = "1.0.0-beta01" androidxComposeRuntimeTracing = "1.0.0-beta01"
androidxCore = "1.12.0" androidxCore = "1.12.0"
androidxCoreSplashscreen = "1.0.1" androidxCoreSplashscreen = "1.0.1"
@ -70,6 +71,7 @@ androidx-compose-foundation = { group = "androidx.compose.foundation", name = "f
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material3-adaptive = { group = "androidx.compose.material3", name = "material3-adaptive", version.ref = "androidxComposeMaterial3Adaptive" }
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" }

Loading…
Cancel
Save