Implement list/detail view with the new material3-adaptive API

Change-Id: I13cca7db13411794e333d34f6edacf594586ef6d
feature/list-detail-pane-scaffold
Miłosz Moczkowski 2 years ago
parent 335a7ec68c
commit dfff80640f

@ -17,14 +17,20 @@
package com.google.samples.apps.nowinandroid.navigation package com.google.samples.apps.nowinandroid.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
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.forYouNavigationRoute import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute
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.interestsGraph
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterestsGraph
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.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicNavigationRoute
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen 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
@ -49,23 +55,35 @@ fun NiaNavHost(
startDestination = startDestination, startDestination = startDestination,
modifier = modifier, modifier = modifier,
) { ) {
forYouScreen(onTopicClick = navController::navigateToTopic) forYouScreen(onTopicClick = navController::navigateToInterestsGraph)
bookmarksScreen( bookmarksScreen(
onTopicClick = navController::navigateToTopic, onTopicClick = navController::navigateToInterestsGraph,
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::navigateToInterestsGraph,
) )
interestsGraph( interestsGraph(
onTopicClick = navController::navigateToTopic, detailsPane = { topicId ->
nestedGraphs = { val nestedNavController = rememberNavController()
topicScreen( NavHost(
onBackClick = navController::popBackStack, navController = nestedNavController,
onTopicClick = navController::navigateToTopic, startDestination = topicNavigationRoute,
) ) {
topicScreen(onTopicClick = nestedNavController::navigateToTopic)
}
LaunchedEffect(topicId) {
nestedNavController.navigateToTopic(
topicId,
navOptions {
popUpTo(nestedNavController.graph.findStartDestination().id) {
inclusive = true
}
},
)
}
}, },
) )
} }

@ -175,7 +175,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.navigateToInterestsGraph(navOptions = topLevelNavOptions)
} }
} }
} }

@ -22,3 +22,9 @@ plugins {
android { android {
namespace = "com.google.samples.apps.nowinandroid.feature.interests" namespace = "com.google.samples.apps.nowinandroid.feature.interests"
} }
dependencies {
implementation(libs.androidx.compose.material3.adaptive) {
isTransitive = false
}
}

@ -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(
selectedTopicId = null,
topics = followableTopicTestData,
),
) )
} }
@ -108,8 +111,9 @@ class InterestsScreenTest {
private fun InterestsScreen(uiState: InterestsUiState) { private fun InterestsScreen(uiState: InterestsUiState) {
InterestsScreen( InterestsScreen(
uiState = uiState, uiState = uiState,
followTopic = { _, _ -> }, followTopic = { _, _ -> /* no-op */ },
onTopicClick = {}, onTopicClick = { /* no-op */ },
detailsPane = { /* no-op */ },
) )
} }
} }

@ -36,7 +36,7 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
@Composable @Composable
internal fun InterestsRoute( internal fun InterestsRoute(
onTopicClick: (String) -> Unit, detailsPane: @Composable (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: InterestsViewModel = hiltViewModel(), viewModel: InterestsViewModel = hiltViewModel(),
) { ) {
@ -45,7 +45,8 @@ internal fun InterestsRoute(
InterestsScreen( InterestsScreen(
uiState = uiState, uiState = uiState,
followTopic = viewModel::followTopic, followTopic = viewModel::followTopic,
onTopicClick = onTopicClick, onTopicClick = viewModel::onTopicClick,
detailsPane = detailsPane,
modifier = modifier, modifier = modifier,
) )
} }
@ -55,6 +56,7 @@ internal fun InterestsScreen(
uiState: InterestsUiState, uiState: InterestsUiState,
followTopic: (String, Boolean) -> Unit, followTopic: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
detailsPane: @Composable (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -67,13 +69,17 @@ internal fun InterestsScreen(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = R.string.loading), contentDesc = stringResource(id = R.string.loading),
) )
is InterestsUiState.Interests -> is InterestsUiState.Interests ->
TopicsTabContent( TopicsTabContent(
topics = uiState.topics, topics = uiState.topics,
selectedTopicId = uiState.selectedTopicId,
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
onFollowButtonClick = followTopic, onFollowButtonClick = followTopic,
detailsPane = detailsPane,
modifier = modifier, modifier = modifier,
) )
is InterestsUiState.Empty -> InterestsEmptyScreen() is InterestsUiState.Empty -> InterestsEmptyScreen()
} }
} }
@ -96,9 +102,11 @@ fun InterestsScreenPopulated(
InterestsScreen( InterestsScreen(
uiState = InterestsUiState.Interests( uiState = InterestsUiState.Interests(
topics = followableTopics, topics = followableTopics,
selectedTopicId = followableTopics.first().topic.id,
), ),
followTopic = { _, _ -> }, followTopic = { _, _ -> },
onTopicClick = {}, onTopicClick = {},
detailsPane = {},
) )
} }
} }
@ -113,6 +121,7 @@ fun InterestsScreenLoading() {
uiState = InterestsUiState.Loading, uiState = InterestsUiState.Loading,
followTopic = { _, _ -> }, followTopic = { _, _ -> },
onTopicClick = {}, onTopicClick = {},
detailsPane = {},
) )
} }
} }
@ -127,6 +136,7 @@ fun InterestsScreenEmpty() {
uiState = InterestsUiState.Empty, uiState = InterestsUiState.Empty,
followTopic = { _, _ -> }, followTopic = { _, _ -> },
onTopicClick = {}, onTopicClick = {},
detailsPane = {},
) )
} }
} }

@ -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.topicIdArg
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 uiState: StateFlow<InterestsUiState> = combine(
getFollowableTopics(sortBy = TopicSortField.NAME).map( savedStateHandle.getStateFlow<String?>(topicIdArg, null),
InterestsUiState::Interests, getFollowableTopics(sortBy = TopicSortField.NAME),
).stateIn( InterestsUiState::Interests,
scope = viewModelScope, ).stateIn(
started = SharingStarted.WhileSubscribed(5_000), scope = viewModelScope,
initialValue = InterestsUiState.Loading, 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) {
viewModelScope.launch {
savedStateHandle[topicIdArg] = 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

@ -16,21 +16,27 @@
package com.google.samples.apps.nowinandroid.feature.interests package com.google.samples.apps.nowinandroid.feature.interests
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.rememberListDetailPaneScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.platform.testTag
@ -40,44 +46,78 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollba
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable @Composable
fun TopicsTabContent( fun TopicsTabContent(
topics: List<FollowableTopic>,
selectedTopicId: String?,
onTopicClick: (String) -> Unit,
onFollowButtonClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier,
detailsPane: @Composable (String) -> Unit,
) {
val listDetailPaneState = rememberListDetailPaneScaffoldState()
BackHandler(enabled = listDetailPaneState.canNavigateBack()) {
listDetailPaneState.navigateBack()
}
LaunchedEffect(selectedTopicId) {
if (selectedTopicId != null) {
listDetailPaneState.navigateTo(ListDetailPaneScaffoldRole.Detail)
}
}
ListDetailPaneScaffold(
scaffoldState = listDetailPaneState,
listPane = {
ListPane(
topics = topics,
onTopicClick = onTopicClick,
onFollowButtonClick = onFollowButtonClick,
)
},
detailPane = {
if (selectedTopicId != null) {
detailsPane(selectedTopicId)
}
},
modifier = modifier,
)
}
@Composable
private fun ListPane(
topics: List<FollowableTopic>, topics: List<FollowableTopic>,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
onFollowButtonClick: (String, Boolean) -> Unit, onFollowButtonClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
withBottomSpacer: Boolean = true,
) { ) {
Box( Box(
modifier = modifier modifier = modifier.fillMaxSize(),
.fillMaxWidth(),
) { ) {
val scrollableState = rememberLazyListState() val scrollableState = rememberLazyListState()
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier.testTag("interests:topics"),
.padding(horizontal = 24.dp)
.testTag("interests:topics"),
contentPadding = PaddingValues(vertical = 16.dp),
state = scrollableState, state = scrollableState,
) { ) {
topics.forEach { followableTopic -> items(
items = topics,
key = { followableTopic -> followableTopic.topic.id },
) { followableTopic ->
val topicId = followableTopic.topic.id val topicId = followableTopic.topic.id
item(key = topicId) { InterestsItem(
InterestsItem( name = followableTopic.topic.name,
name = followableTopic.topic.name, following = followableTopic.isFollowed,
following = followableTopic.isFollowed, description = followableTopic.topic.shortDescription,
description = followableTopic.topic.shortDescription, topicImageUrl = followableTopic.topic.imageUrl,
topicImageUrl = followableTopic.topic.imageUrl, onClick = { onTopicClick(topicId) },
onClick = { onTopicClick(topicId) }, onFollowButtonClick = { onFollowButtonClick(topicId, it) },
onFollowButtonClick = { onFollowButtonClick(topicId, it) }, )
)
}
} }
if (withBottomSpacer) { item {
item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
} }
} }
val scrollbarState = scrollableState.scrollbarState( val scrollbarState = scrollableState.scrollbarState(

@ -16,31 +16,40 @@
package com.google.samples.apps.nowinandroid.feature.interests.navigation package com.google.samples.apps.nowinandroid.feature.interests.navigation
import androidx.compose.runtime.Composable
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.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" internal const val topicIdArg = "topicId"
const val interestsRoute = "interests_route" const val interestsRoute = "interests_route?$topicIdArg={$topicIdArg}"
fun NavController.navigateToInterestsGraph(navOptions: NavOptions? = null) { fun NavController.navigateToInterestsGraph(
this.navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions) topicId: String? = null,
navOptions: NavOptions? = null,
) {
if (topicId == null) {
navigate("interests_route", navOptions)
} else {
navigate("interests_route?$topicIdArg=$topicId", navOptions)
}
} }
fun NavGraphBuilder.interestsGraph( fun NavGraphBuilder.interestsGraph(
onTopicClick: (String) -> Unit, detailsPane: @Composable (String) -> Unit,
nestedGraphs: NavGraphBuilder.() -> Unit,
) { ) {
navigation( composable(
route = INTERESTS_GRAPH_ROUTE_PATTERN, route = interestsRoute,
startDestination = interestsRoute, arguments = listOf(
navArgument(topicIdArg) {
defaultValue = null
nullable = true
},
),
) { ) {
composable(route = interestsRoute) { InterestsRoute(detailsPane)
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
@ -53,6 +54,7 @@ class InterestsViewModelTest {
@Before @Before
fun setup() { fun setup() {
viewModel = InterestsViewModel( viewModel = InterestsViewModel(
savedStateHandle = SavedStateHandle(),
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
getFollowableTopics = getFollowableTopicsUseCase, getFollowableTopics = getFollowableTopicsUseCase,
) )
@ -93,7 +95,7 @@ class InterestsViewModelTest {
) )
assertEquals( assertEquals(
InterestsUiState.Interests(topics = testOutputTopics), InterestsUiState.Interests(selectedTopicId = null, topics = testOutputTopics),
viewModel.uiState.value, viewModel.uiState.value,
) )
@ -123,7 +125,7 @@ class InterestsViewModelTest {
) )
assertEquals( assertEquals(
InterestsUiState.Interests(topics = testInputTopics), InterestsUiState.Interests(selectedTopicId = null, topics = testInputTopics),
viewModel.uiState.value, viewModel.uiState.value,
) )

@ -55,7 +55,6 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
@ -75,7 +74,6 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Success(testTopic), topicUiState = TopicUiState.Success(testTopic),
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
@ -100,7 +98,6 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Success(userNewsResourcesTestData), newsUiState = NewsUiState.Success(userNewsResourcesTestData),
onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
@ -123,7 +120,6 @@ class TopicScreenTest {
newsUiState = NewsUiState.Success( newsUiState = NewsUiState.Success(
userNewsResourcesTestData, userNewsResourcesTestData,
), ),
onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onTopicClick = {}, onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },

@ -32,12 +32,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -57,7 +54,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadi
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
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.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
@ -69,8 +65,7 @@ import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems
import com.google.samples.apps.nowinandroid.feature.topic.R.string import com.google.samples.apps.nowinandroid.feature.topic.R.string
@Composable @Composable
internal fun TopicRoute( fun TopicRoute(
onBackClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TopicViewModel = hiltViewModel(), viewModel: TopicViewModel = hiltViewModel(),
@ -83,7 +78,6 @@ internal fun TopicRoute(
topicUiState = topicUiState, topicUiState = topicUiState,
newsUiState = newsUiState, newsUiState = newsUiState,
modifier = modifier, modifier = modifier,
onBackClick = onBackClick,
onFollowClick = viewModel::followTopicToggle, onFollowClick = viewModel::followTopicToggle,
onBookmarkChanged = viewModel::bookmarkNews, onBookmarkChanged = viewModel::bookmarkNews,
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) }, onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
@ -96,7 +90,6 @@ internal fun TopicRoute(
internal fun TopicScreen( internal fun TopicScreen(
topicUiState: TopicUiState, topicUiState: TopicUiState,
newsUiState: NewsUiState, newsUiState: NewsUiState,
onBackClick: () -> Unit,
onFollowClick: (Boolean) -> Unit, onFollowClick: (Boolean) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
onBookmarkChanged: (String, Boolean) -> Unit, onBookmarkChanged: (String, Boolean) -> Unit,
@ -112,9 +105,6 @@ internal fun TopicScreen(
state = state, state = state,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
item {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing))
}
when (topicUiState) { when (topicUiState) {
TopicUiState.Loading -> item { TopicUiState.Loading -> item {
NiaLoadingWheel( NiaLoadingWheel(
@ -127,7 +117,6 @@ internal fun TopicScreen(
is TopicUiState.Success -> { is TopicUiState.Success -> {
item { item {
TopicToolbar( TopicToolbar(
onBackClick = onBackClick,
onFollowClick = onFollowClick, onFollowClick = onFollowClick,
uiState = topicUiState.followableTopic, uiState = topicUiState.followableTopic,
) )
@ -270,24 +259,12 @@ private fun TopicBodyPreview() {
private fun TopicToolbar( private fun TopicToolbar(
uiState: FollowableTopic, uiState: FollowableTopic,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackClick: () -> Unit = {},
onFollowClick: (Boolean) -> Unit = {}, onFollowClick: (Boolean) -> Unit = {},
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
) { ) {
IconButton(onClick = { onBackClick() }) {
Icon(
imageVector = NiaIcons.ArrowBack,
contentDescription = stringResource(
id = com.google.samples.apps.nowinandroid.core.ui.R.string.back,
),
)
}
val selected = uiState.isFollowed val selected = uiState.isFollowed
NiaFilterChip( NiaFilterChip(
selected = selected, selected = selected,
@ -314,7 +291,6 @@ 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),
onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
onNewsResourceViewed = {}, onNewsResourceViewed = {},
@ -332,7 +308,6 @@ fun TopicScreenLoading() {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
onNewsResourceViewed = {}, onNewsResourceViewed = {},

@ -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.NavOptions
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,29 +33,27 @@ private val URL_CHARACTER_ENCODING = UTF_8.name()
@VisibleForTesting @VisibleForTesting
internal const val topicIdArg = "topicId" internal const val topicIdArg = "topicId"
const val topicNavigationRoute = "topic_route/{$topicIdArg}"
internal class TopicArgs(val topicId: String) { internal class TopicArgs(val topicId: String) {
constructor(savedStateHandle: SavedStateHandle) : constructor(savedStateHandle: SavedStateHandle) :
this(URLDecoder.decode(checkNotNull(savedStateHandle[topicIdArg]), URL_CHARACTER_ENCODING)) this(URLDecoder.decode(checkNotNull(savedStateHandle[topicIdArg]), URL_CHARACTER_ENCODING))
} }
fun NavController.navigateToTopic(topicId: String) { fun NavController.navigateToTopic(topicId: String, navOptions: NavOptions? = null) {
val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING)
this.navigate("topic_route/$encodedId") { navigate("topic_route/$encodedId", navOptions)
launchSingleTop = true
}
} }
fun NavGraphBuilder.topicScreen( fun NavGraphBuilder.topicScreen(
onBackClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
) { ) {
composable( composable(
route = "topic_route/{$topicIdArg}", route = topicNavigationRoute,
arguments = listOf( arguments = listOf(
navArgument(topicIdArg) { type = NavType.StringType }, navArgument(topicIdArg) { type = NavType.StringType },
), ),
) { ) {
TopicRoute(onBackClick = onBackClick, onTopicClick = onTopicClick) TopicRoute(onTopicClick = onTopicClick)
} }
} }

Loading…
Cancel
Save