From 82a8f98c388190d0ee58b4fbb8eb4e6c45137e95 Mon Sep 17 00:00:00 2001 From: Manuel Vivo Date: Wed, 8 Sep 2021 16:39:52 +0200 Subject: [PATCH] [Jetsnack] Snackbar support (#645) --- .../example/jetsnack/model/SnackbarManager.kt | 50 ++++++++++++++++ .../com/example/jetsnack/ui/JetsnackApp.kt | 39 +++++++++++- .../jetsnack/ui/components/Snackbar.kt | 55 +++++++++++++++++ .../com/example/jetsnack/ui/home/cart/Cart.kt | 4 +- .../jetsnack/ui/home/cart/CartViewModel.kt | 60 +++++++++++++++---- Jetsnack/app/src/main/res/values/strings.xml | 2 + .../example/jetsnack/buildsrc/Dependencies.kt | 2 +- 7 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt create mode 100644 Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt new file mode 100644 index 0000000000..4098d4057c --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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.example.jetsnack.model + +import androidx.annotation.StringRes +import java.util.UUID +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +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 } + } + } +} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt index 6e353adb14..76fb31bdc9 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt @@ -17,15 +17,24 @@ package com.example.jetsnack.ui import androidx.compose.foundation.layout.padding +import androidx.compose.material.SnackbarHost +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.navigation.compose.rememberNavController +import com.example.jetsnack.model.SnackbarManager import com.example.jetsnack.ui.components.JetsnackScaffold +import com.example.jetsnack.ui.components.JetsnackSnackbar import com.example.jetsnack.ui.home.HomeSections import com.example.jetsnack.ui.home.JetsnackBottomBar import com.example.jetsnack.ui.theme.JetsnackTheme import com.google.accompanist.insets.ProvideWindowInsets +import com.google.accompanist.insets.systemBarsPadding @Composable fun JetsnackApp() { @@ -33,14 +42,42 @@ fun JetsnackApp() { JetsnackTheme { val tabs = remember { HomeSections.values() } val navController = rememberNavController() + val scaffoldState = rememberScaffoldState() + JetsnackScaffold( - bottomBar = { JetsnackBottomBar(navController = navController, tabs = tabs) } + bottomBar = { JetsnackBottomBar(navController = navController, tabs = tabs) }, + snackbarHost = { + SnackbarHost( + hostState = it, + modifier = Modifier.systemBarsPadding(), + snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) } + ) + }, + scaffoldState = scaffoldState ) { innerPaddingModifier -> JetsnackNavGraph( navController = navController, modifier = Modifier.padding(innerPaddingModifier) ) } + + // Handle Snackbar messages + val currentMessages by SnackbarManager.messages.collectAsState() + if (currentMessages.isNotEmpty()) { + val message = currentMessages[0] + val messageText: String = stringResource(message.messageId) + + // Effect running in a coroutine that displays the Snackbar on the screen + // If there's a change to messageText, SnackbarManager, or scaffoldState, the + // previous effect will be cancelled and a new one will start with the new values + LaunchedEffect(messageText, SnackbarManager, scaffoldState) { + // Display the snackbar on the screen. `showSnackbar` is a function + // that suspends until the snackbar disappears from the screen + scaffoldState.snackbarHostState.showSnackbar(messageText) + // Once the snackbar is gone or dismissed, notify the SnackbarManager + SnackbarManager.setMessageShown(message.id) + } + } } } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt new file mode 100644 index 0000000000..0c745c2875 --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2021 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.example.jetsnack.ui.components + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Snackbar +import androidx.compose.material.SnackbarData +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.jetsnack.ui.theme.JetsnackTheme + +/** + * An alternative to [androidx.compose.material.Snackbar] utilizing + * [com.example.jetsnack.ui.theme.JetsnackColors] + */ +@Composable +fun JetsnackSnackbar( + snackbarData: SnackbarData, + modifier: Modifier = Modifier, + actionOnNewLine: Boolean = false, + shape: Shape = MaterialTheme.shapes.small, + backgroundColor: Color = JetsnackTheme.colors.uiBackground, + contentColor: Color = JetsnackTheme.colors.textSecondary, + actionColor: Color = JetsnackTheme.colors.brand, + elevation: Dp = 6.dp +) { + Snackbar( + snackbarData = snackbarData, + modifier = modifier, + actionOnNewLine = actionOnNewLine, + shape = shape, + backgroundColor = backgroundColor, + contentColor = contentColor, + actionColor = actionColor, + elevation = elevation + ) +} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt index 95df90f0db..9b690dbc01 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt @@ -87,9 +87,9 @@ import com.google.accompanist.insets.statusBarsHeight @Composable fun Cart( onSnackClick: (Long) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: CartViewModel = viewModel(factory = CartViewModel.provideFactory()) ) { - val viewModel: CartViewModel = viewModel() val orderLines by viewModel.orderLines.collectAsState() val inspiredByCart = remember { SnackRepo.getInspiredByCart() } Cart( diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt index 59d7492631..bf9b9c98aa 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt @@ -17,8 +17,11 @@ package com.example.jetsnack.ui.home.cart import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.jetsnack.R import com.example.jetsnack.model.OrderLine import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.model.SnackbarManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,31 +30,47 @@ import kotlinx.coroutines.flow.StateFlow * * TODO: Move data to Repository so it can be displayed and changed consistently throughout the app. */ -class CartViewModel : ViewModel() { +class CartViewModel( + private val snackbarManager: SnackbarManager, + snackRepository: SnackRepo +) : ViewModel() { + private val _orderLines: MutableStateFlow> = - MutableStateFlow(SnackRepo.getCart()) + MutableStateFlow(snackRepository.getCart()) val orderLines: StateFlow> get() = _orderLines - fun removeSnack(snackId: Long) { - _orderLines.value = _orderLines.value.filter { it.snack.id != snackId } - } + // Logic to show errors every few requests + private var requestCount = 0 + private fun shouldRandomlyFail(): Boolean = ++requestCount % 5 == 0 fun increaseSnackCount(snackId: Long) { - val currentCount = _orderLines.value.first { it.snack.id == snackId }.count - updateSnackCount(snackId, currentCount + 1) + if (!shouldRandomlyFail()) { + val currentCount = _orderLines.value.first { it.snack.id == snackId }.count + updateSnackCount(snackId, currentCount + 1) + } else { + snackbarManager.showMessage(R.string.cart_increase_error) + } } fun decreaseSnackCount(snackId: Long) { - val currentCount = _orderLines.value.first { it.snack.id == snackId }.count - if (currentCount == 1) { - // remove snack from cart - removeSnack(snackId) + if (!shouldRandomlyFail()) { + val currentCount = _orderLines.value.first { it.snack.id == snackId }.count + if (currentCount == 1) { + // remove snack from cart + removeSnack(snackId) + } else { + // update quantity in cart + updateSnackCount(snackId, currentCount - 1) + } } else { - // update quantity in cart - updateSnackCount(snackId, currentCount - 1) + snackbarManager.showMessage(R.string.cart_decrease_error) } } + fun removeSnack(snackId: Long) { + _orderLines.value = _orderLines.value.filter { it.snack.id != snackId } + } + private fun updateSnackCount(snackId: Long, count: Int) { _orderLines.value = _orderLines.value.map { if (it.snack.id == snackId) { @@ -61,4 +80,19 @@ class CartViewModel : ViewModel() { } } } + + /** + * Factory for CartViewModel that takes SnackbarManager as a dependency + */ + companion object { + fun provideFactory( + snackbarManager: SnackbarManager = SnackbarManager, + snackRepository: SnackRepo = SnackRepo + ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return CartViewModel(snackbarManager, snackRepository) as T + } + } + } } diff --git a/Jetsnack/app/src/main/res/values/strings.xml b/Jetsnack/app/src/main/res/values/strings.xml index 69a5aa4e45..5faa4cd6ed 100644 --- a/Jetsnack/app/src/main/res/values/strings.xml +++ b/Jetsnack/app/src/main/res/values/strings.xml @@ -53,6 +53,8 @@ Shipping & Handling Total Checkout + There was an error and the quantity couldn\'t be increased. Please try again. + There was an error and the quantity couldn\'t be decreased. Please try again. Remove item diff --git a/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt b/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt index 5c95f28753..d1da30de67 100644 --- a/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt +++ b/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt @@ -38,7 +38,7 @@ object Libs { } object Coroutines { - private const val version = "1.5.1" + private const val version = "1.5.2" const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version" const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version" const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version"