How to handle one-shot operations in Jetpack Compose?

Issue

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.

Solution

I prefer using SharedFlow for a job like this:

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")
    }
}

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.

Answered By – Phil Dukhov

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published