Skip to content

Commit

Permalink
[Jetsnack] Snackbar support (android#645)
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelvicnt authored Sep 8, 2021
1 parent f60792e commit 82a8f98
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -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<List<Message>> = MutableStateFlow(emptyList())
val messages: StateFlow<List<Message>> 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 }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,67 @@
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() {
ProvideWindowInsets {
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)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<List<OrderLine>> =
MutableStateFlow(SnackRepo.getCart())
MutableStateFlow(snackRepository.getCart())
val orderLines: StateFlow<List<OrderLine>> 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) {
Expand All @@ -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 <T : ViewModel?> create(modelClass: Class<T>): T {
return CartViewModel(snackbarManager, snackRepository) as T
}
}
}
}
2 changes: 2 additions & 0 deletions Jetsnack/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
<string name="cart_shipping_label">Shipping &amp; Handling</string>
<string name="cart_total_label">Total</string>
<string name="cart_checkout">Checkout</string>
<string name="cart_increase_error">There was an error and the quantity couldn\'t be increased. Please try again.</string>
<string name="cart_decrease_error">There was an error and the quantity couldn\'t be decreased. Please try again.</string>
<string name="label_remove">Remove item</string>

<!-- Quantity Selector -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 82a8f98

Please sign in to comment.