Seeing as I have multiple places where snackbars could be triggered, I want to have a central place in my app where I can handle showing/dismissing snackbars.
This is the structure of my app:
I've implemented a BaseViewModel that contains a StateFlow which should keep track of the SnackBar message (every other ViewModel inherits from this BaseViewModel):
#HiltViewModel
open class BaseViewModel #Inject constructor() : ViewModel() {
val _snackBarMessage = MutableStateFlow("")
val snackBarMessage: StateFlow<String> = _snackBarMessage
}
To test if the update of the StateFlow is triggered correctly, I've implemented a message that should update the StateFlow after every login:
private fun setSnackBarMessage() {
_snackBarMessage.value = "A wild snackBar appeared"
}
MainContent contains my Scaffold (incl. scaffoldState, snackbarHost), should react to changes in the snackBarMessage flow and display/dismiss the Snackbar when needed:
fun MainContent(...){
val message by viewModel.snackBarMessage.collectAsState()
LaunchedEffect(message) {
if (message.isNotEmpty() Timber.d("We got a snackbar")
}
Scaffold(...){...}
}
During debugging, I noticed that after every login the snackBarMessage value is updated correctly but MainContent does not get those updates which, in turn, means that the snackbar is never displayed.
Is there a reason why MainContent does not get those updates from the LoginComposable?
Is it even possible to have a central instance of a snackbar or do I really need to handle snackbars separately in every Composable?
You can use this
#Composable
fun MainScreen() {
val coroutineScope = rememberCoroutineScope()
val showSnackBar: (
message: String?,
actionLabel: String,
actionPerformed: () -> Unit,
dismissed: () -> Unit
) -> Unit = { message, actionLabel, actionPerformed, dismissed ->
coroutineScope.launch {
val snackBarResult = scaffoldState.snackbarHostState.showSnackbar(
message = message.toString(),
actionLabel = actionLabel
)
when (snackBarResult) {
SnackbarResult.ActionPerformed -> actionPerformed.invoke()
SnackbarResult.Dismissed -> dismissed.invoke()
}
}
}
//Global using
showSnackBar.invoke(
"YOUR_MESSAGE",
"ACTION_LABEL",
{
//TODO ON ACTION PERFORMED
},
{
//TODO ON DISMISSED
}
)
}
Probably, the cause of your problem is using message as the key for LaunchedEffect and not changing the message at the same time. In the documentation you can read that type of side-effect will be re-launched after key modification.
If LaunchedEffect is recomposed with different keys (see the
Restarting Effects section below), the existing coroutine will be
cancelled and the new suspend function will be launched in a new
coroutine.
Some effects in Compose, like LaunchedEffect, produceState, or
DisposableEffect, take a variable number of arguments, keys, that are
used to cancel the running effect and start a new one with the new
keys.
I suggest wrapping snackbar message in some kind of object (not data class) with field containing snackbar content.
Cheers
Related
In a Jetpack Compose application, I have two composables similar to here:
#Composable
fun Main() {
println("Composed Main")
val context = LocalContext.current
var text by remember { mutableStateOf("") }
fun update(num: Number) {
text = num.toString()
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
Column {
Text(text)
Keypad { update(it) }
}
}
#Composable
fun Keypad(onClick: (Number) -> Unit) {
println("Composed Keypad")
Column {
for (i in 1..10) {
Button(onClick = {onClick(i)}) {
Text(i.toString())
}
}
}
}
Clicking each button causes the two composables to recompose and produces this output:
I/System.out: Composed Main
I/System.out: Composed Keypad
Recomposing the Keypad composable is unneeded and makes the app freeze (for several seconds in a bigger project).
Removing usages of context in the event handles (in here, commenting out the Toast) solves the problem and does not recompose the Keypad and produces this output:
I/System.out: Composed Main
Is there any other way I could use context in an event without causing unneeded recompositions?
The issue is the Context not being a stable (#Stable) type. The lambda/callback of KeyPad is updating a state and its immediately followed by a component that uses an unstable Context, this results to the onClickLambda to be re-created (you can see its hashcode changing everytime you click a button), thus making the Keypad composable not skippable.
You can consider four approaches to deal with your issue. I also made some changes to your code removing the local function and put everything directly in the lambda/callback to make everything smaller.
For the first two, start first by creating a generic wrapper class like this.
#Stable
data class StableWrapper<T>(val value: T)
Wrapping Context in the #Stable wrapper
Using the generic wrapper class, you can consider wrapping the context and use it like this
#Composable
fun Main() {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
val context = LocalContext.current
val contextStableWrapper = StableWrapper(context)
Column {
Text(text)
Keypad {
text = it.toString()
Toast.makeText(contextStableWrapper.value, "Toast", Toast.LENGTH_SHORT).show()
}
}
}
Wrapping your Toast in the #Stable wrapper
Toast is also an unstable type, so you have to make it "stable" with this second approach.
Note that this only applies if your Toast message will not change.
Hoist them up above your Main where you'll create an instance of your static-message Toast and put it inside the stable wrapper
val toastWrapper = StableWrapper(
Toast.makeText(LocalContext.current, "Toast", Toast.LENGTH_SHORT)
)
Main(toastWrapper = toastWrapper)
and your Main composable will look like this
#Composable
fun Main(toastWrapper: StableWrapper<Toast>) {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
Column {
Text(text)
Keypad {
text = it.toString()
toastWrapper.value.show()
}
}
}
remember{…} the Context
(I might expect some correction here), I think this is called "memoizing the value (Context) inside remember{…}", this looks similar to a deferred read.
#Composable
fun Main() {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
val context = LocalContext.current
val rememberedContext = remember { { context } }
Column {
Text(text)
Keypad {
text = it.toString()
Toast.makeText(rememberedContext(), "Toast", Toast.LENGTH_SHORT).show()
}
}
}
Use Side-Effects
You can utilize Compose Side-Effects and put the Toast in them.
Here, SideEffect will execute every post-recomposition.
SideEffect {
if (text.isNotEmpty()) {
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
}
or you can utilize LaunchedEffect using the text as its key, so on succeeding re-compositions, when the text changes, different from its previous value (invalidates), the LaunchedEffect will re-execute and show the toast again
LaunchedEffect(key1 = text) {
if (text.isNotEmpty()) {
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
}
Replacing your print with Log statements, this is the output of any of the approaches when clicking the buttons
E/Composable: Composed Main // first launch of screen
E/Composable: Composed Keypad // first launch of screen
// succeeding clicks
E/Composable: Composed Main
E/Composable: Composed Main
E/Composable: Composed Main
E/Composable: Composed Main
The only part I'm still not sure of is the first approach, even if Toast is not a stable type based on the second, just wrapping the context in the stable wrapper in the first approach is sufficient enough for the Keypad composable to get skipped.
I have a LiveData in my ViewModel:-
private val _toastMessage = MutableLiveData<Long>()
val toastMessage
get() = _toastMessage
And this is the only way I am changing it's value(on click of a submit button in the fragment):-
fun onSubmitClicked(<params>){
Log.i(LOG_TAG, "submit button clicked")
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded())
{
_toastMessage.value = 0
}else{
_toastMessage.value = 1
}
}
}
And in the fragment, I have an observer for this LiveData:-
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
when{
(it.compareTo(0) == 0) -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_not_inserted), Toast.LENGTH_SHORT).show()
else -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_inserted), Toast.LENGTH_SHORT).show()
}
})
Ideally, I am expecting the onChange of this Observer to be called only on clicking the submit button on my fragment. But, as I can see, it is also getting called even on onCreateView of my fragment.
What could be the possible reasons for this?
The issue is that LiveData pushes new values while you're observing it, but it also pushes the most recent value when you first observe it, or if the observer's Lifecycle resumes and the data has changed since it was paused.
So when you set toastMessage's value to 1, it stays that way - and ViewModels have a longer lifetime than Fragments (that's the whole point!) so when your Fragment gets recreated, it observes the current value of toastMessage, sees that it's currently 1, and shows a Toast.
The problem is you don't want to use it as a persistent data state - you want it to be a one-shot event that you consume when you observe it, so the Toast is only shown once in response to a button press. This is one of the tricky things about LiveData and there have been a bunch of workarounds, classes, libraries etc built around making it work
There's an old post here from one of the Android developers discussing the problem with this use case, and the workarounds available and where they fall short - in case anyone is interested! But like it says at the top, that's all outdated, and they recommend following the official guidelines.
The official way basically goes:
something triggers an event on the ViewModel
the VM updates the UI state including a message to be displayed
the UI observes this update, displays the message, and informs the VM it's been displayed
the VM updates the UI state with the message removed
That's not the only way to handle consumable events, but it's what they're recommending, and it's fairly simple. So you'd want to do something like this:
// making this nullable so we can have a "no message" state
private val _toastMessage = MutableLiveData<Long?>(null)
// you should specify the type here btw, as LiveData instead of MutableLiveData -
// that's the reason for making the Mutable reference private and having a public version
val toastMessage: LiveData<Long?>
get() = _toastMessage
// call this when the current message has been shown
fun messageDisplayed() {
_toastMessage.value = null
}
// make a nice display function to avoid repetition
fun displayToast(#StringRes resId: Int) {
Toast.makeText(context, resources.getString(resId), Toast.LENGTH_SHORT).show()
// remember to tell the VM it's been displayed
transactionViewModel.messageDisplayed()
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
// if the message code is null we just don't do anything
when(it) {
0 -> displayToast(R.string.toast_msg_transaction_not_inserted)
1 -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
You also might want to create an enum of Toast states instead of just using numbers, way more readable - you can even put their string IDs in the enum:
enum class TransactionMessage(#StringRes val stringId: Int) {
INSERTED(R.string.toast_msg_transaction_inserted),
NOT_INSERTED(R.string.toast_msg_transaction_not_inserted)
}
private val _toastMessage = MutableLiveData<TransactionMessage?>(null)
val toastMessage: LiveData<TransactionMessage?>
get() = _toastMessage
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded()) toastMessage.value = NOT_INSERTED
else _toastMessage.value = INSERTED
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { message ->
message?.let { displayToast(it.stringId) }
// or if you're not putting the string resource IDs in the enum:
when(message) {
NOT_INSERTED -> displayToast(R.string.toast_msg_transaction_not_inserted)
INSERTED -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
It can be a bit clearer and self-documenting compared to just using numbers, y'know?
I want to send a channel value from viewmodel and collect it in MainActivity for every compose component screen.In this context I found a solution like this.
I defined a channel in my core viewmodel like below
val snackBarErrorMessageChannel = Channel<SnackbarMessageEvent>()
val snackBarErrorMessage = snackBarErrorMessageChannel.receiveAsFlow()
Then I collect it in MainActivity onCreate method in setContent like below
LaunchedEffect(key1 = Unit) {
viewModel.snackBarErrorMessage.collect {
kutuphanemAppState.showSnackbar(
it.message,
it.duration,
it.type
)
}
}
When I collect it in my screen snackbar is showing but when I want to call for all compose screen it is not trigger. Is there any wrong? Where can I collect for all my compose screen?
I'm writing an app with Jetpack Compose that lets the user input text in some TextFields and check a few radio buttons.
This data is then stored in a Room database.
Currently, I have a "save" button at the bottom of the screen, with a "Leave without saving?" popup.
However, I'd like to get rid of the save button entirely and have it autosave data as it's being typed.
Would the repeated DB queries from typing cause any performance issues? And are there any established best practices for this sort of thing?
With kotlin flow you can use debounce, which is designed specifically for such cases. That way, as long as the user enters text, saveToDatabase will not be called, and when he does not enter a character for some time (in my example it is one second) - the flow will be emitted.
Also during Compose Navigation the view model may be destroyed (and the coroutine will be cancelled) if the screen is closed, in that case I also save the data inside onCleared to make sure that nothing is missing.
class ScreenViewModel: ViewModel() {
private val _text = MutableStateFlow("")
val text: StateFlow<String> = _text
init {
viewModelScope.launch {
#OptIn(FlowPreview::class)
_text.debounce(1000)
.collect(::saveToDatabase)
}
}
fun updateText(text: String) {
_text.value = text
}
override fun onCleared() {
super.onCleared()
saveToDatabase(_text.value)
}
private fun saveToDatabase(text: String) {
}
}
#Composable
fun ScreenView(
viewModel: ScreenViewModel = viewModel()
) {
val text by viewModel.text.collectAsState()
TextField(value = text, onValueChange = viewModel::updateText)
}
#OptIn(FlowPreview::class) means that the API may be changed in the future. If you don't want to use it now, see the replacement here.
I am creating demo project for using jetpack compose with mvvm , i have created model class that holds the list of users.. those users are displayed in list and there is a button at top which adds new user to the list when clicked...
when user clicks on the button an the lambda updates activity about it and activity calls viewmodel which adds data to list and updates back to activity using livedata, now after the model receives the new data it does not update composable function about it and hence ui of list is not updated..
here is the code
#Model
data class UsersState(var users: ArrayList<UserModel> = ArrayList())
Activity
class MainActivity : AppCompatActivity() {
private val usersState: UsersState = UsersState()
private val usersListViewModel: UsersListViewModel = UsersListViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usersListViewModel.getUsers().observe(this, Observer {
usersState.users.addAll(it)
})
usersListViewModel.addUsers()
setContent {
UsersListUi.addList(
usersState,
onAddClick = { usersListViewModel.addNewUser() },
onRemoveClick = { usersListViewModel.removeFirstUser() })
}
}
}
ViewModel
class UsersListViewModel {
private val usersList: MutableLiveData<ArrayList<UserModel>> by lazy {
MutableLiveData<ArrayList<UserModel>>()
}
private val users: ArrayList<UserModel> = ArrayList()
fun addUsers() {
users.add(UserModel("jon", "doe", "android developer"))
users.add(UserModel("john", "doe", "flutter developer"))
users.add(UserModel("jonn", "dove", "ios developer"))
usersList.value = users
}
fun getUsers(): MutableLiveData<ArrayList<UserModel>> {
return usersList
}
fun addNewUser() {
users.add(UserModel("jony", "dove", "ruby developer"))
usersList.value = users
}
fun removeFirstUser() {
if (!users.isNullOrEmpty()) {
users.removeAt(0)
usersList.value = users
}
}
}
composable function
#Composable
fun addList(state: UsersState, onAddClick: () -> Unit, onRemoveClick: () -> Unit) {
MaterialTheme {
FlexColumn {
inflexible {
// Item height will be equal content height
TopAppBar( // App Bar with title
title = { Text("Users") }
)
FlexRow() {
expanded(flex = 1f) {
Button(
text = "add",
onClick = { onAddClick.invoke() },
style = OutlinedButtonStyle()
)
}
expanded(flex = 1f) {
Button(
text = "sub",
onClick = { onRemoveClick.invoke() },
style = OutlinedButtonStyle()
)
}
}
VerticalScroller {
Column {
state.users.forEach {
Column {
Row {
Text(text = it.userName)
WidthSpacer(width = 2.dp)
Text(text = it.userSurName)
}
Text(text = it.userJob)
}
Divider(color = Color.Black, height = 1.dp)
}
}
}
}
}
}
}
the whole source code is available here
I am not sure if i am doing something wrong or is it because jetpack compose is still in developers preview , so would appreciate any help..
thank you
Ahoy!
Sean from Android Devrel here. The main reason this isn't updating is the ArrayList in UserState.users is not observable – it's just a regular ArrayList so mutating it won't update compose.
Model makes all properties of the model class observable
It seems like this might work because UserState is annotated #Model, which makes things automatically observable by Compose. However, the observability only applies one level deep. Here's an example that would never trigger recomposition:
class ModelState(var username: String, var email: String)
#Model
class MyImmutableModel(val state: ModelState())
Since the state variable is immutable (val), Compose will never trigger recompositions when you change the email or username. This is because #Model only applies to the properties of the class annotated. In this example state is observable in Compose, but username and email are just regular strings.
Fix Option #0: You don't need #Model
In this case you already have a LiveData from getUsers() – you can observe that in compose. We haven't shipped a Compose observation yet in the dev releases, but it's possible to write one using effects until we ship a observation method. Just remember to remove the observer in onDispose {}.
This is also true if you're using any other observable type, like Flow, Flowable, etc. You can pass them directly into #Composable functions and observe them with effects without introducing an intermediate #Model class.
Fix Option #1: Using immutable types in #Model
A lot of developers prefer immutable data types for UI state (patterns like MVI encourage this). You can update your example to use immutable lists, then in order to change the list you'll have to assign to the users property which will be observable by Compose.
#Model
class UsersState(var users: List<UserModel> = listOf())
Then when you want to update it you have to assign the users variable:
val usersState = UsersState()
// ...
fun addUsers(newUsers: List<UserModel>) {
usersState.users = usersState.users + newUsers
// performance note: note this allocates a new list every time on the main thread
// which may be OK if this is rarely called and lists are small
// it's too expensive for large lists or if this is called often
}
This will always trigger recomposition any time a new List<UserModel is assigned to users, and since there's no way to edit the list after it's been assigned the UI will always show the current state.
In this case, since the data structure is a List that you're concatenating the performance of immutable types may not be acceptable. However, if you're holding an immutable data class this option is a good one so I included it for completeness.
Fix Option #2: Using ModelList
Compose has a special observable list type for exactly this use case. You can use instead of an ArrayList and any changes to the list will be observable by compose.
#Model
class UsersState(val users: ModelList<UserModel> = ModelList())
If you use ModelList the rest of the code you've written in the Activity will work correctly and Compose will be able to observe changes to users directly.
Related: Nesting #Model classes
It's worth noting that you can nest #Model classes, which is how the ModelList version works. Going back to the example at the beginning, if you annotate both classes as #Model, then all of the properties will be observable in Compose.
#Model
class ModelState(var username: String, var email: String)
#Model
class MyModel(var state: ModelState())
Note: This version adds #Model to ModelState, and also allows reassignment of state in MyModel
Since #Model makes all of the properties of the class that is annotated observable by compose, state, username, and email will all be observable.
TL;DR which option to choose
Avoiding #Model (Option #0) completely in this code will avoid introducing a duplicate model layer just for Compose. Since you're already holding state in a ViewModel and exposing it via LiveData you can just pass the LiveData directly to compose and observe it there. This would be my first choice.
If you do want to use #Model to represent a mutable list, then use ModelList from Option #2.
You'll probably want to change the ViewModel to hold a MutableLiveData reference as well. Currently the list held by the ViewModel is not observable. For an introduction to ViewModel and LiveData from Android Architecture components check out the Android Basics course.
Your model is not observed so changes won't be reflected.
In this article under the section 'Putting it all together' the List is added.
val list = +memo{ calculation: () -> T}
Example for your list:
#Composable
fun test(supplier: UserState) {
val list = +memo{supplier.users}
ListConsumer(list){
/* Do other stuff for your usecase */
}
}