Search code examples
androidkotlineventsandroid-viewmodelandroid-jetpack-compose

Jetpack Compose show snack bar from view model - Single Live Event


I'm building a jetpack compose app and I want my view model to tell my compose function to display a snack bar by sending it an event. I have read multiple blog posts about the Single Live Event case with Kotlin and I tried to implement it with Compose and Kotlin Flow. I managed to send the event from the view model (I see it in the logs) but I don't know how to receive it in the composable function. Can someone help me figure it out please? Here is my implementation.

class HomeViewModel() : ViewModel() {
    sealed class Event {
        object ShowSheet : Event()
        object HideSheet : Event()
        data class ShowSnackBar(val text: String) : Event()
    }

    private val eventChannel = Channel<Event>(Channel.BUFFERED)
    val eventsFlow: Flow<Event> = eventChannel.receiveAsFlow()

    fun showSnackbar() {
        Timber.d("Show snackbar button pressed")
        viewModelScope.launch {
            eventChannel.send(Event.ShowSnackBar("SnackBar"))
        }
    }
}
@Composable
fun HomeScreen(
    viewModel: HomeViewModel,
) {
    val context = LocalContext.current

    val scaffoldState = rememberScaffoldState()
    val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)

    val lifecycleOwner = LocalLifecycleOwner.current
    val eventsFlowLifecycleAware = remember(viewModel.eventsFlow, lifecycleOwner) {
        eventsFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
    }

    LaunchedEffect(sheetState, scaffoldState.snackbarHostState) {
        eventsFlowLifecycleAware.onEach {
            when (it) {
                HomeViewModel.Event.ShowSheet -> {
                    Timber.d("Show sheet event received")
                    sheetState.show()
                }
                HomeViewModel.Event.HideSheet -> {
                    Timber.d("Hide sheet event received")
                    sheetState.hide()
                }
                is HomeViewModel.Event.ShowSnackBar -> {
                    Timber.d("Show snack bar received")
                    scaffoldState.snackbarHostState.showSnackbar(
                        context.getString(it.resId)
                    )
                }
            }
        }
    }

    ModalBottomSheetLayout(
        sheetState = sheetState,
        sheetContent = {
            Text("Sheet")
        }
    ) {
        Button(
            onClick = {
                viewModel.showSheet()
            }
        ) {
            Text("Show SnackBar")
        }
    }
}

For reference, I've used these blog posts:


Solution

  • Ok, I was using the wrong approach, I must not send events, I must update the view state and check if I should show the snackbar when recomposing. Something like that:

    You store the SnackBar state in the view model

    class HomeViewModel: ViewModel() {
        var isSnackBarShowing: Boolean by mutableStateOf(false)
            private set
    
        private fun showSnackBar() {
            isSnackBarShowing = true
        }
    
        fun dismissSnackBar() {
            isSnackBarShowing = false
        }
    }
    

    And in the view you use LaunchedEffect to check if you should show the snackbar when recomposing the view

    @Composable
    fun HomeScreen(
        viewModel: HomeViewModel,
    ) {
        val onDismissSnackBarState by rememberUpdatedState(newValue = onDismissSnackBar)
    
        if (isSnackBarShowing) {
            val snackBarMessage = "Message"
            LaunchedEffect(isSnackBarShowing) {
                try {
                    when (scaffoldState.snackbarHostState.showSnackbar(
                        snackBarMessage,
                    )) {
                        SnackbarResult.Dismissed -> {
                        }
                    }
                } finally {
                    onDismissSnackBarState()
                }
            }
        }
    
        Row() {
            Text(text = "Hello")
            Spacer(modifier = Modifier.weight(1f))
            Button(
                onClick = {
                    viewModel.showSnackBar()
                }
            ) {
                Text(text = "Show SnackBar")
            }
        }
    }