From e39585d5d20cd069fd15913aae3bb0c98f894108 Mon Sep 17 00:00:00 2001 From: James Shvarts Date: Tue, 8 Nov 2022 21:16:00 -0500 Subject: [PATCH] Add Snackbar handling --- .../conditionalbottomnav/MainActivity.kt | 92 +++++++++++++++---- .../jshvarts/conditionalbottomnav/Screens.kt | 16 +++- .../model/SnackbarManager.kt | 35 +++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/jshvarts/conditionalbottomnav/model/SnackbarManager.kt diff --git a/app/src/main/java/com/jshvarts/conditionalbottomnav/MainActivity.kt b/app/src/main/java/com/jshvarts/conditionalbottomnav/MainActivity.kt index e0adc79..4be4c08 100644 --- a/app/src/main/java/com/jshvarts/conditionalbottomnav/MainActivity.kt +++ b/app/src/main/java/com/jshvarts/conditionalbottomnav/MainActivity.kt @@ -1,18 +1,19 @@ package com.jshvarts.conditionalbottomnav +import android.content.res.Resources import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -25,11 +26,14 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.jshvarts.conditionalbottomnav.model.SnackbarManager import com.jshvarts.conditionalbottomnav.navigation.BottomBarTab import com.jshvarts.conditionalbottomnav.navigation.HOME_GRAPH import com.jshvarts.conditionalbottomnav.navigation.HomeDestinations.HOME_ITEM_ROUTE import com.jshvarts.conditionalbottomnav.navigation.navGraph import com.jshvarts.conditionalbottomnav.ui.theme.ConditionalBottomNavTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -58,18 +62,26 @@ class MainActivity : ComponentActivity() { @Composable fun App() { ConditionalBottomNavTheme { - val appState = rememberAppState() - Scaffold( - bottomBar = { - if (appState.shouldShowBottomBar) { - BottomBar( - tabs = appState.bottomBarTabs, - currentRoute = appState.currentRoute!!, - navigateToRoute = appState::navigateToBottomBarRoute - ) - } - } - ) { innerPaddingModifier -> +val appState = rememberAppState() +Scaffold( + bottomBar = { + if (appState.shouldShowBottomBar) { + BottomBar( + tabs = appState.bottomBarTabs, + currentRoute = appState.currentRoute!!, + navigateToRoute = appState::navigateToBottomBarRoute + ) + } + }, + snackbarHost = { + SnackbarHost( + hostState = it, + modifier = Modifier.systemBarsPadding(), + snackbar = { snackbarData -> Snackbar(snackbarData) } + ) + }, + scaffoldState = appState.scaffoldState +) { innerPaddingModifier -> NavHost( navController = appState.navController, startDestination = HOME_GRAPH, @@ -86,16 +98,45 @@ fun App() { @Composable fun rememberAppState( - navController: NavHostController = rememberNavController() + scaffoldState: ScaffoldState = rememberScaffoldState(), + navController: NavHostController = rememberNavController(), + snackbarManager: SnackbarManager = SnackbarManager, + resources: Resources = resources(), + coroutineScope: CoroutineScope = rememberCoroutineScope() ) = - remember(navController) { - AppState(navController) + remember(scaffoldState, navController, snackbarManager, resources, coroutineScope) { + AppState(scaffoldState, navController, snackbarManager, resources, coroutineScope) } @Stable class AppState( - val navController: NavHostController + val scaffoldState: ScaffoldState, + val navController: NavHostController, + private val snackbarManager: SnackbarManager, + private val resources: Resources, + coroutineScope: CoroutineScope ) { + // Process snackbars coming from SnackbarManager + init { + coroutineScope.launch { + snackbarManager.messages.collect { currentMessages -> + if (currentMessages.isNotEmpty()) { + val message = currentMessages[0] + val text = resources.getText(message.messageId) + + // Display the snackbar on the screen. `showSnackbar` is a function + // that suspends until the snackbar disappears from the screen + scaffoldState.snackbarHostState.showSnackbar(text.toString()) + // Once the snackbar is gone or dismissed, notify the SnackbarManager + snackbarManager.setMessageShown(message.id) + } + } + } + } + + // ---------------------------------------------------------- + // BottomBar state source of truth + // ---------------------------------------------------------- val bottomBarTabs = BottomBarTab.values() private val bottomBarRoutes = bottomBarTabs.map { it.route } @@ -140,6 +181,17 @@ private tailrec fun findStartDestination(graph: NavDestination): NavDestination return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph } +/** + * A composable function that returns the [Resources]. It will be recomposed when `Configuration` + * gets updated. + */ +@Composable +@ReadOnlyComposable +private fun resources(): Resources { + LocalConfiguration.current + return LocalContext.current.resources +} + @Composable fun BottomBar( tabs: Array, diff --git a/app/src/main/java/com/jshvarts/conditionalbottomnav/Screens.kt b/app/src/main/java/com/jshvarts/conditionalbottomnav/Screens.kt index 9a1a063..2bd6d08 100644 --- a/app/src/main/java/com/jshvarts/conditionalbottomnav/Screens.kt +++ b/app/src/main/java/com/jshvarts/conditionalbottomnav/Screens.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.jshvarts.conditionalbottomnav.model.SnackbarManager +import kotlin.random.Random @Composable fun HomeScreen( @@ -98,10 +100,14 @@ fun HomeItemDetailScreen( .padding(innerPaddingModifier) .fillMaxSize() ) { - Text( - text = "Home Item $itemId", - style = MaterialTheme.typography.h6 - ) + if (ifRandomlyFailed()) { + SnackbarManager.showMessage(R.string.item_lookup_failed_error) + } else { + Text( + text = "Home Item $itemId", + style = MaterialTheme.typography.h6 + ) + } } } } @@ -128,3 +134,5 @@ fun HomeItemCard( ) } } + +private fun ifRandomlyFailed() = Random.nextBoolean() diff --git a/app/src/main/java/com/jshvarts/conditionalbottomnav/model/SnackbarManager.kt b/app/src/main/java/com/jshvarts/conditionalbottomnav/model/SnackbarManager.kt new file mode 100644 index 0000000..14511ad --- /dev/null +++ b/app/src/main/java/com/jshvarts/conditionalbottomnav/model/SnackbarManager.kt @@ -0,0 +1,35 @@ +package com.jshvarts.conditionalbottomnav.model + +import androidx.annotation.StringRes +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.* + + +data class Message(val id: Long, @StringRes val messageId: Int) + +/** + * Class responsible for managing Snackbar messages to show on the screen + */ +object SnackbarManager { + + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + val messages: StateFlow> get() = _messages.asStateFlow() + + fun showMessage(@StringRes messageTextId: Int) { + _messages.update { currentMessages -> + currentMessages + Message( + id = UUID.randomUUID().mostSignificantBits, + messageId = messageTextId + ) + } + } + + fun setMessageShown(messageId: Long) { + _messages.update { currentMessages -> + currentMessages.filterNot { it.id == messageId } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d80f1d0..10990a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,5 @@ Chat Item Detail Close + Item lookup failed \ No newline at end of file