Abhimanyu
Abhimanyu

Reputation: 14827

How to handle one-shot operations in Jetpack Compose?

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

Answers (2)

Phil Dukhov
Phil Dukhov

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

alexal1
alexal1

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

Related Questions