Update General Error Handling Implementation

Include OFFLINE message in list of messages, with precedence over other messages. Pass offline value to state for use in other purposes.

Include action handling for success/failure to click snackbar button. Implementation of Bookmark UNDO function.

Moved implementations of onShowSnackbar and errorHandler closer to use case, removed unnecessary passed down parameters.
pull/1461/head
TM 1 year ago
parent e776036f7d
commit d25152b8d1

@ -76,7 +76,6 @@ class NiaAppStateTest {
NiaAppState( NiaAppState(
navController = navController, navController = navController,
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
@ -99,7 +98,6 @@ class NiaAppStateTest {
fun niaAppState_destinations() = runTest { fun niaAppState_destinations() = runTest {
composeTestRule.setContent { composeTestRule.setContent {
state = rememberNiaAppState( state = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
@ -118,7 +116,6 @@ class NiaAppStateTest {
state = NiaAppState( state = NiaAppState(
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
@ -129,7 +126,7 @@ class NiaAppStateTest {
networkMonitor.setConnected(false) networkMonitor.setConnected(false)
assertEquals( assertEquals(
true, true,
state.isOffline.value, state.isOfflineState.value,
) )
} }
@ -139,7 +136,6 @@ class NiaAppStateTest {
state = NiaAppState( state = NiaAppState(
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
@ -160,14 +156,13 @@ class NiaAppStateTest {
state = NiaAppState( state = NiaAppState(
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )
} }
val id = state.addErrorMessage("Test Error Message 1") val id = state.addShortErrorMessage("Test Error Message 1")
backgroundScope.launch { state.snackbarMessage.collect() } backgroundScope.launch { state.snackbarMessage.collect() }

@ -136,7 +136,6 @@ class MainActivity : ComponentActivity() {
} }
val appState = rememberNiaAppState( val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,

@ -38,12 +38,11 @@ import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetai
@Composable @Composable
fun NiaNavHost( fun NiaNavHost(
appState: NiaAppState, appState: NiaAppState,
onShowSnackbar: suspend (String, String?) -> Boolean,
errorHandler: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startDestination: String = FOR_YOU_ROUTE, startDestination: String = FOR_YOU_ROUTE,
) { ) {
val navController = appState.navController val navController = appState.navController
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = startDestination, startDestination = startDestination,
@ -52,13 +51,13 @@ fun NiaNavHost(
forYouScreen(onTopicClick = navController::navigateToInterests) forYouScreen(onTopicClick = navController::navigateToInterests)
bookmarksScreen( bookmarksScreen(
onTopicClick = navController::navigateToInterests, onTopicClick = navController::navigateToInterests,
onShowSnackbar = onShowSnackbar, onShowSnackbar = { message, label, actionSuccess, actionFailure -> appState.addLongErrorMessage(error = message, label = label, successAction = actionSuccess, failureAction = actionFailure) },
) )
searchScreen( searchScreen(
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) }, onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
onTopicClick = navController::navigateToInterests, onTopicClick = navController::navigateToInterests,
errorHandler = errorHandler, errorHandler = { message -> appState.addShortErrorMessage(message) },
) )
interestsListDetailScreen() interestsListDetailScreen()
} }

@ -30,6 +30,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarDuration.Short import androidx.compose.material3.SnackbarDuration.Short
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
@ -42,6 +43,7 @@ import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -62,6 +64,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMessage
import com.google.samples.apps.nowinandroid.core.data.util.MessageDuration
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationSuiteScaffold import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationSuiteScaffold
@ -95,30 +99,24 @@ fun NiaApp(
) { ) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val isOffline by appState.isOffline.collectAsStateWithLifecycle() val offlineMessage = stringResource(R.string.not_connected)
SideEffect {
appState.offlineMessage = offlineMessage
}
val snackbarMessage by appState.snackbarMessage.collectAsStateWithLifecycle() val snackbarMessage by appState.snackbarMessage.collectAsStateWithLifecycle()
// If user is not connected to the internet show a snack bar to inform them.
val notConnectedMessage = stringResource(R.string.not_connected)
LaunchedEffect(isOffline) {
if (isOffline) {
snackbarHostState.showSnackbar(
message = notConnectedMessage,
duration = Indefinite,
)
}
}
LaunchedEffect(snackbarMessage) { LaunchedEffect(snackbarMessage) {
snackbarMessage?.let { snackbarMessage?.let {
val snackBarResult = snackbarHostState.showSnackbar( val snackBarResult = snackbarHostState.showSnackbar(
message = it.message, message = it.message,
actionLabel = "Continue", actionLabel = it.label,
duration = Indefinite, duration = snackbarDurationOf(it.duration),
) == ActionPerformed ) == ActionPerformed
if (snackBarResult) { handleSnackbarResult(snackBarResult, it)
appState.clearErrorMessage(it.id) // Remove Message from Queue
} appState.clearErrorMessage(it.id)
} }
} }
@ -243,14 +241,6 @@ internal fun NiaApp(
) { ) {
NiaNavHost( NiaNavHost(
appState = appState, appState = appState,
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,
actionLabel = action,
duration = Short,
) == ActionPerformed
},
errorHandler = { message -> appState.addErrorMessage(message) },
) )
} }
@ -284,3 +274,20 @@ private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLev
this?.hierarchy?.any { this?.hierarchy?.any {
it.route?.contains(destination.name, true) ?: false it.route?.contains(destination.name, true) ?: false
} ?: false } ?: false
private fun snackbarDurationOf(duration: MessageDuration?): SnackbarDuration {
return when (duration) {
MessageDuration.Short -> SnackbarDuration.Short
MessageDuration.Long -> SnackbarDuration.Long
MessageDuration.Indefinite -> SnackbarDuration.Indefinite
else -> SnackbarDuration.Short
}
}
private fun handleSnackbarResult(snackBarResult: Boolean, message: ErrorMessage) {
if (snackBarResult) {
message.actionPerformed?.invoke()
} else {
message.actionNotPerformed?.invoke()
}
}

@ -29,8 +29,8 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions import androidx.navigation.navOptions
import androidx.tracing.trace import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMessage
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BOOKMARKS_ROUTE import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BOOKMARKS_ROUTE
@ -48,13 +48,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
@Composable @Composable
fun rememberNiaAppState( fun rememberNiaAppState(
networkMonitor: NetworkMonitor,
errorMonitor: ErrorMonitor, errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
@ -65,7 +63,6 @@ fun rememberNiaAppState(
return remember( return remember(
navController, navController,
coroutineScope, coroutineScope,
networkMonitor,
errorMonitor, errorMonitor,
userNewsResourceRepository, userNewsResourceRepository,
timeZoneMonitor, timeZoneMonitor,
@ -73,7 +70,6 @@ fun rememberNiaAppState(
NiaAppState( NiaAppState(
navController = navController, navController = navController,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
@ -85,7 +81,6 @@ fun rememberNiaAppState(
class NiaAppState( class NiaAppState(
val navController: NavHostController, val navController: NavHostController,
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor,
errorMonitor: ErrorMonitor, errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
@ -102,21 +97,17 @@ class NiaAppState(
else -> null else -> null
} }
val isOffline = networkMonitor.isOnline val isOfflineState: StateFlow<Boolean> = isOffline.stateIn(
.map(Boolean::not) scope = coroutineScope,
.stateIn( started = SharingStarted.WhileSubscribed(5_000),
scope = coroutineScope, initialValue = false,
started = SharingStarted.WhileSubscribed(5_000), )
initialValue = false,
) val snackbarMessage: StateFlow<ErrorMessage?> = errorMessage.stateIn(
scope = coroutineScope,
val snackbarMessage = errorMessages started = SharingStarted.WhileSubscribed(5_000),
.map { it.firstOrNull() } initialValue = null,
.stateIn( )
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)
/** /**
* Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the

@ -138,7 +138,6 @@ class NiaAppScreenSizesScreenshotTests {
) { ) {
NiaTheme { NiaTheme {
val fakeAppState = rememberNiaAppState( val fakeAppState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,

@ -218,7 +218,6 @@ class SnackbarScreenshotTests {
BoxWithConstraints { BoxWithConstraints {
NiaTheme { NiaTheme {
val appState = rememberNiaAppState( val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor, errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,

@ -22,7 +22,11 @@ import kotlinx.coroutines.flow.Flow
* Interface for handling error messages. * Interface for handling error messages.
*/ */
interface ErrorMonitor { interface ErrorMonitor {
fun addErrorMessage(error: String): String? fun addShortErrorMessage(error: String, label: String? = null, successAction: (() -> Unit)? = null, failureAction: (() -> Unit)? = null): String?
fun addLongErrorMessage(error: String, label: String? = null, successAction: (() -> Unit)? = null, failureAction: (() -> Unit)? = null): String?
fun addIndefiniteErrorMessage(error: String, label: String? = null, successAction: (() -> Unit)? = null, failureAction: (() -> Unit)? = null): String?
fun clearErrorMessage(id: String) fun clearErrorMessage(id: String)
val errorMessages: Flow<List<ErrorMessage?>> val errorMessage: Flow<ErrorMessage?>
val isOffline: Flow<Boolean>
var offlineMessage: String?
} }

@ -18,6 +18,8 @@ package com.google.samples.apps.nowinandroid.core.data.util
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -26,12 +28,25 @@ import javax.inject.Inject
* Interface implementation for handling general errors. * Interface implementation for handling general errors.
*/ */
class SnackbarErrorMonitor @Inject constructor() : ErrorMonitor { class SnackbarErrorMonitor @Inject constructor(val networkMonitor: NetworkMonitor) : ErrorMonitor {
/** /**
* List of [ErrorMessage] to be shown to the user, via Snackbar. * List of [ErrorMessage] to be shown to the user, via Snackbar.
*/ */
private val _errorMessages = MutableStateFlow<List<ErrorMessage>>(emptyList()) private val errorMessages = MutableStateFlow<List<ErrorMessage>>(emptyList())
override val errorMessages: Flow<List<ErrorMessage>> = _errorMessages
override val isOffline = networkMonitor.isOnline
.map(Boolean::not)
override var offlineMessage: String? = null
override val errorMessage: Flow<ErrorMessage?> = combine(errorMessages, isOffline) { messages, isOffline ->
// Offline Error Message takes precedence over other messages
if (isOffline) {
ErrorMessage(offlineMessage ?: "You are offline", duration = MessageDuration.Indefinite)
} else {
messages.firstOrNull()
}
}
/** /**
* Creates an [ErrorMessage] from String value and adds it to the list. * Creates an [ErrorMessage] from String value and adds it to the list.
@ -41,20 +56,32 @@ class SnackbarErrorMonitor @Inject constructor() : ErrorMonitor {
* Returns the ID of the new [ErrorMessage] if success * Returns the ID of the new [ErrorMessage] if success
* Returns null if [error] is Blank * Returns null if [error] is Blank
*/ */
override fun addErrorMessage(error: String): String? { private fun addErrorMessage(error: String, label: String?, duration: MessageDuration?, actionPerformed: (() -> Unit)?, actionNotPerformed: (() -> Unit)?): String? {
if (error.isNotBlank()) { if (error.isNotBlank()) {
val newError = ErrorMessage(error) val newError = ErrorMessage(error, label = label, duration = duration, actionPerformed = actionPerformed, actionNotPerformed = actionNotPerformed)
_errorMessages.update { it + newError } errorMessages.update { it + newError }
return newError.id return newError.id
} }
return null return null
} }
override fun addShortErrorMessage(error: String, label: String?, successAction: (() -> Unit)?, failureAction: (() -> Unit)?): String? {
return addErrorMessage(error, label, MessageDuration.Short, successAction, failureAction)
}
override fun addLongErrorMessage(error: String, label: String?, successAction: (() -> Unit)?, failureAction: (() -> Unit)?): String? {
return addErrorMessage(error, label, MessageDuration.Long, successAction, failureAction)
}
override fun addIndefiniteErrorMessage(error: String, label: String?, successAction: (() -> Unit)?, failureAction: (() -> Unit)?): String? {
return addErrorMessage(error, label, MessageDuration.Indefinite, successAction, failureAction)
}
/** /**
* Removes the [ErrorMessage] with the specified [id] from the list. * Removes the [ErrorMessage] with the specified [id] from the list.
*/ */
override fun clearErrorMessage(id: String) { override fun clearErrorMessage(id: String) {
_errorMessages.update { it.filter { item -> item.id != id } } errorMessages.update { it.filter { item -> item.id != id } }
} }
} }
@ -64,4 +91,14 @@ class SnackbarErrorMonitor @Inject constructor() : ErrorMonitor {
data class ErrorMessage( data class ErrorMessage(
val message: String, val message: String,
val id: String = UUID.randomUUID().toString(), val id: String = UUID.randomUUID().toString(),
val label: String? = null,
val duration: MessageDuration? = MessageDuration.Short,
val actionPerformed: (() -> Unit)? = null,
val actionNotPerformed: (() -> Unit)? = null,
) )
enum class MessageDuration {
Short,
Long,
Indefinite,
}

@ -55,7 +55,7 @@ class BookmarksScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
BookmarksScreen( BookmarksScreen(
feedState = NewsFeedUiState.Loading, feedState = NewsFeedUiState.Loading,
onShowSnackbar = { _, _ -> false }, onShowSnackbar = { _, _, _, _ -> Unit },
removeFromBookmarks = {}, removeFromBookmarks = {},
onTopicClick = {}, onTopicClick = {},
onNewsResourceViewed = {}, onNewsResourceViewed = {},
@ -76,7 +76,7 @@ class BookmarksScreenTest {
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
userNewsResourcesTestData.take(2), userNewsResourcesTestData.take(2),
), ),
onShowSnackbar = { _, _ -> false }, onShowSnackbar = { _, _, _, _ -> Unit },
removeFromBookmarks = {}, removeFromBookmarks = {},
onTopicClick = {}, onTopicClick = {},
onNewsResourceViewed = {}, onNewsResourceViewed = {},
@ -117,7 +117,7 @@ class BookmarksScreenTest {
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
userNewsResourcesTestData.take(2), userNewsResourcesTestData.take(2),
), ),
onShowSnackbar = { _, _ -> false }, onShowSnackbar = { _, _, _, _ -> Unit },
removeFromBookmarks = { newsResourceId -> removeFromBookmarks = { newsResourceId ->
assertEquals(userNewsResourcesTestData[0].id, newsResourceId) assertEquals(userNewsResourcesTestData[0].id, newsResourceId)
removeFromBookmarksCalled = true removeFromBookmarksCalled = true
@ -152,7 +152,7 @@ class BookmarksScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
BookmarksScreen( BookmarksScreen(
feedState = NewsFeedUiState.Success(emptyList()), feedState = NewsFeedUiState.Success(emptyList()),
onShowSnackbar = { _, _ -> false }, onShowSnackbar = { _, _, _, _ -> Unit },
removeFromBookmarks = {}, removeFromBookmarks = {},
onTopicClick = {}, onTopicClick = {},
onNewsResourceViewed = {}, onNewsResourceViewed = {},
@ -181,7 +181,7 @@ class BookmarksScreenTest {
CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) { CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) {
BookmarksScreen( BookmarksScreen(
feedState = NewsFeedUiState.Success(emptyList()), feedState = NewsFeedUiState.Success(emptyList()),
onShowSnackbar = { _, _ -> false }, onShowSnackbar = { _, _, _, _ -> Unit },
removeFromBookmarks = {}, removeFromBookmarks = {},
onTopicClick = {}, onTopicClick = {},
onNewsResourceViewed = {}, onNewsResourceViewed = {},

@ -78,7 +78,7 @@ import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@Composable @Composable
internal fun BookmarksRoute( internal fun BookmarksRoute(
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
onShowSnackbar: suspend (String, String?) -> Boolean, onShowSnackbar: (String, String?, (() -> Unit)?, (() -> Unit)?) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: BookmarksViewModel = hiltViewModel(), viewModel: BookmarksViewModel = hiltViewModel(),
) { ) {
@ -103,7 +103,7 @@ internal fun BookmarksRoute(
@Composable @Composable
internal fun BookmarksScreen( internal fun BookmarksScreen(
feedState: NewsFeedUiState, feedState: NewsFeedUiState,
onShowSnackbar: suspend (String, String?) -> Boolean, onShowSnackbar: (String, String?, (() -> Unit)?, (() -> Unit)?) -> Unit,
removeFromBookmarks: (String) -> Unit, removeFromBookmarks: (String) -> Unit,
onNewsResourceViewed: (String) -> Unit, onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
@ -117,12 +117,7 @@ internal fun BookmarksScreen(
LaunchedEffect(shouldDisplayUndoBookmark) { LaunchedEffect(shouldDisplayUndoBookmark) {
if (shouldDisplayUndoBookmark) { if (shouldDisplayUndoBookmark) {
val snackBarResult = onShowSnackbar(bookmarkRemovedMessage, undoText) onShowSnackbar(bookmarkRemovedMessage, undoText, { undoBookmarkRemoval() }, { clearUndoState() })
if (snackBarResult) {
undoBookmarkRemoval()
} else {
clearUndoState()
}
} }
} }

@ -28,7 +28,7 @@ fun NavController.navigateToBookmarks(navOptions: NavOptions) = navigate(BOOKMAR
fun NavGraphBuilder.bookmarksScreen( fun NavGraphBuilder.bookmarksScreen(
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
onShowSnackbar: suspend (String, String?) -> Boolean, onShowSnackbar: (String, String?, (() -> Unit)?, (() -> Unit)?) -> Unit,
) { ) {
composable(route = BOOKMARKS_ROUTE) { composable(route = BOOKMARKS_ROUTE) {
BookmarksRoute(onTopicClick, onShowSnackbar) BookmarksRoute(onTopicClick, onShowSnackbar)

Loading…
Cancel
Save