Reputation: 14827
Note - This question is pretty much the same as this one.
I am looking for a better approach if there is any.
As per Android Docs,
Once the transient message has been shown, the UI needs to notify the ViewModel of that, causing another UI state update:
For example, when I show a Toast message on a button click, should the UI notify the ViewModel that the toast is shown successfully?
Is that the intended best approach to handle one-shot operations like toasts, snackbar, etc?
Sample code,
@Composable
fun OneShotOperation(
viewmodel: OneShotOperationViewModel = viewModel(),
) {
val context = LocalContext.current
LaunchedEffect(
key1 = viewmodel.toastMessage,
) {
if (viewmodel.toastMessage.isNotBlank()) {
Toast.makeText(
context,
viewmodel.toastMessage,
Toast.LENGTH_SHORT,
).show()
viewmodel.toastMessage = "" // Is this the correct way?
}
}
Button(
onClick = {
viewmodel.toastMessage = "Sample Toast"
},
) {
Text(text = "Show Toast")
}
}
class OneShotOperationViewModel : ViewModel() {
var toastMessage by mutableStateOf(
value = "",
)
}
If I remove this line,
viewmodel.toastMessage = "" // Is this the correct way?
The toast is shown only once, the subsequent button press do not show a toast since the mutable state has not changed.
Upvotes: 17
Views: 11922
Reputation: 87914
I prefer using SharedFlow
for a job like this.
Note that if you send a message from another view while collect
is not running, it will not show toast when you finally start it. It does not store the value, it only passes it on to all the collectors that are currently connected.
class OneShotOperationViewModel : ViewModel() {
private val _toastMessage = MutableSharedFlow<String>()
val toastMessage = _toastMessage.asSharedFlow()
fun sendMessage(message: String) {
viewModelScope.launch {
_toastMessage.emit(message)
}
}
}
@Composable
fun TestScreen() {
val context = LocalContext.current
val viewModel = viewModel<OneShotOperationViewModel>()
LaunchedEffect(Unit) {
viewModel
.toastMessage
.collect { message ->
Toast.makeText(
context,
message,
Toast.LENGTH_SHORT,
).show()
}
}
Button(
onClick = {
viewModel.sendMessage("Sample Toast")
},
) {
Text(text = "Show Toast")
}
}
Upvotes: 52
Reputation: 433
Instead of using
viewmodel.toastMessage = "" // Is this the correct way?
you can wrap your message into a simple wrapper:
class ToastMessageWrapper(val value: String)
and use this class in mutableStateOf()
:
val toastMessage: ToastMessageWrapper by mutableStateOf(null)
Then LaunchedEffect
will get updated on every new value. Note that I'm using class
, not data class
. Because we have to keep these classes different each time even for same values.
Upvotes: 0