NavHost replaced with if statement

pull/1823/head
Ivan 8 months ago
parent a0f2786634
commit 7a4eb0d9d3

@ -17,46 +17,24 @@
package com.google.samples.apps.nowinandroid.ui.interests2pane package com.google.samples.apps.nowinandroid.ui.interests2pane
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.annotation.Keep
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.AnimatedPane import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
import kotlinx.serialization.Serializable
import java.util.UUID
@Serializable internal object TopicPlaceholderRoute
// TODO: Remove @Keep when https://issuetracker.google.com/353898971 is fixed
@Keep
@Serializable internal object DetailPaneNavHostRoute
fun NavGraphBuilder.interestsListDetailScreen() { fun NavGraphBuilder.interestsListDetailScreen() {
composable<InterestsRoute> { composable<InterestsRoute> {
@ -64,31 +42,16 @@ fun NavGraphBuilder.interestsListDetailScreen() {
} }
} }
@Composable
internal fun InterestsListDetailScreen(
viewModel: Interests2PaneViewModel = hiltViewModel(),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) {
val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()
InterestsListDetailScreen(
selectedTopicId = selectedTopicId,
onTopicClick = viewModel::onTopicClick,
windowAdaptiveInfo = windowAdaptiveInfo,
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class) @OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable @Composable
internal fun InterestsListDetailScreen( internal fun InterestsListDetailScreen(
selectedTopicId: String?, viewModel: Interests2PaneViewModel = hiltViewModel()
onTopicClick: (String) -> Unit,
windowAdaptiveInfo: WindowAdaptiveInfo,
) { ) {
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator( val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()
scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo), val listDetailNavigator = rememberListDetailPaneScaffoldNavigator<String>(
initialDestinationHistory = listOfNotNull( initialDestinationHistory = listOfNotNull(
ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List), ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List),
ThreePaneScaffoldDestinationItem<Nothing>(ListDetailPaneScaffoldRole.Detail).takeIf { ThreePaneScaffoldDestinationItem<String>(ListDetailPaneScaffoldRole.Detail).takeIf {
selectedTopicId != null selectedTopicId != null
}, },
), ),
@ -97,33 +60,9 @@ internal fun InterestsListDetailScreen(
listDetailNavigator.navigateBack() listDetailNavigator.navigateBack()
} }
var nestedNavHostStartRoute by remember { fun onTopicClickShowDetailPane(selectedTopicId: String) {
val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute viewModel.onTopicClick(selectedTopicId)
mutableStateOf(route) listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, selectedTopicId)
}
var nestedNavKey by rememberSaveable(
stateSaver = Saver({ it.toString() }, UUID::fromString),
) {
mutableStateOf(UUID.randomUUID())
}
val nestedNavController = key(nestedNavKey) {
rememberNavController()
}
fun onTopicClickShowDetailPane(topicId: String) {
onTopicClick(topicId)
if (listDetailNavigator.isDetailPaneVisible()) {
// If the detail pane was visible, then use the nestedNavController navigate call
// directly
nestedNavController.navigateToTopic(topicId) {
popUpTo<DetailPaneNavHostRoute>()
}
} else {
// Otherwise, recreate the NavHost entirely, and start at the new destination
nestedNavHostStartRoute = TopicRoute(id = topicId)
nestedNavKey = UUID.randomUUID()
}
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
} }
ListDetailPaneScaffold( ListDetailPaneScaffold(
@ -139,22 +78,15 @@ internal fun InterestsListDetailScreen(
}, },
detailPane = { detailPane = {
AnimatedPane { AnimatedPane {
key(nestedNavKey) { if (selectedTopicId != null) {
NavHost( TopicScreen(
navController = nestedNavController, topicId = selectedTopicId!!,
startDestination = nestedNavHostStartRoute, showBackButton = !listDetailNavigator.isListPaneVisible(),
route = DetailPaneNavHostRoute::class, onBackClick = listDetailNavigator::navigateBack,
) { onTopicClick = ::onTopicClickShowDetailPane,
topicScreen( )
showBackButton = !listDetailNavigator.isListPaneVisible(), } else
onBackClick = listDetailNavigator::navigateBack, TopicDetailPlaceholder()
onTopicClick = ::onTopicClickShowDetailPane,
)
composable<TopicPlaceholderRoute> {
TopicDetailPlaceholder()
}
}
}
} }
}, },
) )

@ -175,18 +175,6 @@ internal fun ForYouScreen(
onboardingUiState = onboardingUiState, onboardingUiState = onboardingUiState,
onTopicCheckedChanged = onTopicCheckedChanged, onTopicCheckedChanged = onTopicCheckedChanged,
saveFollowedTopics = saveFollowedTopics, saveFollowedTopics = saveFollowedTopics,
// Custom LayoutModifier to remove the enforced parent 16.dp contentPadding
// from the LazyVerticalGrid and enable edge-to-edge scrolling for this section
interestsItemModifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(
constraints.copy(
maxWidth = constraints.maxWidth + 32.dp.roundToPx(),
),
)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
},
) )
newsFeed( newsFeed(
@ -258,17 +246,29 @@ private fun LazyStaggeredGridScope.onboarding(
onboardingUiState: OnboardingUiState, onboardingUiState: OnboardingUiState,
onTopicCheckedChanged: (String, Boolean) -> Unit, onTopicCheckedChanged: (String, Boolean) -> Unit,
saveFollowedTopics: () -> Unit, saveFollowedTopics: () -> Unit,
interestsItemModifier: Modifier = Modifier,
) { ) {
when (onboardingUiState) { when (onboardingUiState) {
OnboardingUiState.Loading, OnboardingUiState.Loading,
OnboardingUiState.LoadFailed, OnboardingUiState.LoadFailed,
OnboardingUiState.NotShown, OnboardingUiState.NotShown,
-> Unit -> Unit
is OnboardingUiState.Shown -> { is OnboardingUiState.Shown -> {
item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") { item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") {
Column(modifier = interestsItemModifier) { // Custom LayoutModifier to remove the enforced parent 16.dp contentPadding
// from the LazyVerticalGrid and enable edge-to-edge scrolling for this section
Column(
modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(
constraints.copy(
maxWidth = constraints.maxWidth + 32.dp.roundToPx(),
),
)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
},
) {
Text( Text(
text = stringResource(R.string.feature_foryou_onboarding_guidance_title), text = stringResource(R.string.feature_foryou_onboarding_guidance_title),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -493,7 +493,7 @@ private fun feedItemsSize(
OnboardingUiState.Loading, OnboardingUiState.Loading,
OnboardingUiState.LoadFailed, OnboardingUiState.LoadFailed,
OnboardingUiState.NotShown, OnboardingUiState.NotShown,
-> 0 -> 0
is OnboardingUiState.Shown -> 1 is OnboardingUiState.Shown -> 1
} }

@ -72,20 +72,24 @@ import com.google.samples.apps.nowinandroid.feature.topic.R.string
@Composable @Composable
fun TopicScreen( fun TopicScreen(
topicId: String,
showBackButton: Boolean, showBackButton: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TopicViewModel = hiltViewModel(), viewModel: TopicViewModel = hiltViewModel()
) { ) {
viewModel.updateTopic(topicId)
val topicUiState: TopicUiState by viewModel.topicUiState.collectAsStateWithLifecycle() val topicUiState: TopicUiState by viewModel.topicUiState.collectAsStateWithLifecycle()
val newsUiState: NewsUiState by viewModel.newsUiState.collectAsStateWithLifecycle() val newsUiState: NewsUiState by viewModel.newsUiState.collectAsStateWithLifecycle()
val selectedTopicId by viewModel.topicId.collectAsStateWithLifecycle()
TrackScreenViewEvent(screenName = "Topic: ${viewModel.topicId}") TrackScreenViewEvent(screenName = "Topic: $selectedTopicId")
TopicScreen( TopicScreen(
topicUiState = topicUiState, topicUiState = topicUiState,
newsUiState = newsUiState, newsUiState = newsUiState,
modifier = modifier.testTag("topic:${viewModel.topicId}"), modifier = modifier.testTag("topic:${selectedTopicId}"),
showBackButton = showBackButton, showBackButton = showBackButton,
onBackClick = onBackClick, onBackClick = onBackClick,
onFollowClick = viewModel::followTopicToggle, onFollowClick = viewModel::followTopicToggle,

@ -16,10 +16,8 @@
package com.google.samples.apps.nowinandroid.feature.topic package com.google.samples.apps.nowinandroid.feature.topic
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
@ -29,52 +27,74 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult import com.google.samples.apps.nowinandroid.core.result.asResult
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class TopicViewModel @Inject constructor( class TopicViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
topicsRepository: TopicsRepository, topicsRepository: TopicsRepository,
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository
) : ViewModel() { ) : ViewModel() {
val topicId = savedStateHandle.toRoute<TopicRoute>().id private val _topicId = MutableStateFlow<String?>(null)
val topicId = _topicId.asStateFlow()
private val _topicUIState = MutableStateFlow<TopicUiState>(TopicUiState.Loading)
private val _newsUIState = MutableStateFlow<NewsUiState>(NewsUiState.Loading)
val topicUiState: StateFlow<TopicUiState> = topicUiState( init {
topicId = topicId, viewModelScope.launch {
userDataRepository = userDataRepository, _topicId.filterNotNull().collect { topicId ->
topicsRepository = topicsRepository, combine(
topicUiState(
topicId = topicId,
userDataRepository = userDataRepository,
topicsRepository = topicsRepository,
),
newsUiState(
topicId = topicId,
userDataRepository = userDataRepository,
userNewsResourceRepository = userNewsResourceRepository,
),
) { topicIUState, newsUIState ->
_topicUIState.update { topicIUState }
_newsUIState.update { newsUIState }
}.stateIn(viewModelScope)
}
}
}
val topicUiState: StateFlow<TopicUiState> = _topicUIState.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TopicUiState.Loading,
) )
.stateIn(
scope = viewModelScope, val newsUiState: StateFlow<NewsUiState> = _newsUIState.stateIn(
started = SharingStarted.WhileSubscribed(5_000), scope = viewModelScope,
initialValue = TopicUiState.Loading, started = SharingStarted.WhileSubscribed(5_000),
) initialValue = NewsUiState.Loading,
val newsUiState: StateFlow<NewsUiState> = newsUiState(
topicId = topicId,
userDataRepository = userDataRepository,
userNewsResourceRepository = userNewsResourceRepository,
) )
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsUiState.Loading,
)
fun followTopicToggle(followed: Boolean) { fun followTopicToggle(followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
userDataRepository.setTopicIdFollowed(topicId, followed) _topicId.value?.let {
userDataRepository.setTopicIdFollowed(it, followed)
}
} }
} }
@ -89,6 +109,10 @@ class TopicViewModel @Inject constructor(
userDataRepository.setNewsResourceViewed(newsResourceId, viewed) userDataRepository.setNewsResourceViewed(newsResourceId, viewed)
} }
} }
fun updateTopic(id: String) {
this._topicId.value = id
}
} }
private fun topicUiState( private fun topicUiState(

@ -36,8 +36,10 @@ fun NavGraphBuilder.topicScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
) { ) {
composable<TopicRoute> { composable<TopicRoute> { entry ->
val id = entry.arguments?.getString("id")!!
TopicScreen( TopicScreen(
topicId = id,
showBackButton = showBackButton, showBackButton = showBackButton,
onBackClick = onBackClick, onBackClick = onBackClick,
onTopicClick = onTopicClick, onTopicClick = onTopicClick,

@ -16,8 +16,6 @@
package com.google.samples.apps.nowinandroid.feature.topic package com.google.samples.apps.nowinandroid.feature.topic
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.testing.invoke
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
@ -26,7 +24,6 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepo
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -70,18 +67,16 @@ class TopicViewModelTest {
@Before @Before
fun setup() { fun setup() {
viewModel = TopicViewModel( viewModel = TopicViewModel(
savedStateHandle = SavedStateHandle(
route = TopicRoute(id = testInputTopics[0].topic.id),
),
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
topicsRepository = topicsRepository, topicsRepository = topicsRepository,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
) )
viewModel.updateTopic(testInputTopics[0].topic.id)
} }
@Test @Test
fun topicId_matchesTopicIdFromSavedStateHandle() = fun topicId_matchesTopicIdFromSavedStateHandle() =
assertEquals(testInputTopics[0].topic.id, viewModel.topicId) assertEquals(testInputTopics[0].topic.id, viewModel.topicId.value)
@Test @Test
fun uiStateTopic_whenSuccess_matchesTopicFromRepository() = runTest { fun uiStateTopic_whenSuccess_matchesTopicFromRepository() = runTest {

Loading…
Cancel
Save