Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • Android SingleLiveEvent Redux with Kotlin Flow

  • A safer way to collect flows from Android UIs

like image 375
Bak Avatar asked Apr 14 '21 23:04

Bak


Video Answer


2 Answers

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")
        }
    }
}
like image 127
Bak Avatar answered Oct 21 '22 17:10

Bak


I think you have to collect eventsFlowLifecycleAware as a state to trigger a Composable correctly.

Try removing the LaunchedEffect block, and using it like this:

val event by eventsFlowLifecycleAware.collectAsState(null)
when (event) {
    is HomeViewModel.Event.ShowSnackBar -> {
        // Do stuff
    }
}
like image 33
Donny Rozendal Avatar answered Oct 21 '22 18:10

Donny Rozendal