Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack Compose how to collect flows from the view model and action them in composables?

ViewModel:

    class RulesViewModel : ViewModel() {
        private val _sharedFlow = MutableSharedFlow<ScreenEvents>()
        val sharedFlow = _sharedFlow.asSharedFlow()

        sealed class ScreenEvents {
            data class ShowSnackbar(val message: String) : ScreenEvents()
            data class Navigate(val route: String) : ScreenEvents()
        }
    }

Composable:

@Composable
fun EventListener(
    rulesVm: RulesViewModel,
) {
    LaunchedEffect(key1 = true) {
        rulesVm.sharedFlow.collect { event ->
            when(event) {
                is RulesViewModel.ScreenEvents.ShowSnackbar -> {
                    SnackbarScreen("snackbar ${event.message}")
                }
                is RulesViewModel.ScreenEvents.Navigate -> {
                    // todo
                }
            }
        }
    }
}

This gives an error message: @Composable invocations can only happen from the context of a @Composable function

What is the best practice for collecting flows then from viewModels and actioning them in composables?

like image 757
Victor Cocuz Avatar asked Oct 22 '25 13:10

Victor Cocuz


2 Answers

It's better to use extension - Flow.collectAsState()

In Your case it will be:

val screenState = rulesVm.sharedFlow.collectAsState

Then in the body of a composable function You go:

@Composable
fun EventListener(
rulesVm: RulesViewModel,
) {
    val screenState = rulesVm.sharedFlow.collectAsState()
    
    when(screenState) {
             is RulesViewModel.ScreenEvents.ShowSnackbar -> {
                 SnackbarScreen("snackbar ${event.message}")
             }
             is RulesViewModel.ScreenEvents.Navigate -> {
                 // todo
             }
    }
}

You can't invocate composable function inside LaunchedEffect body and inside Flow.collect() because it's an extension on coroutine scope, not a composable function.

like image 172
Андрей Утко Avatar answered Oct 24 '25 06:10

Андрей Утко


I think what you would like to see is the following behavior: you dispatch an event and the snackbar appears on the current screen without going to the another screen. If you do not need to take into account the application lifecycle when receiving events, then the example below will suit you. I create a scaffoldState, which I use to display the Snaskbar. The subscription occurs once due to the fact that Unit is an object.

val scaffoldState = rememberScaffoldState()
LaunchedEffect(Unit) {
    rulesVm.sharedFlow.collect { event ->
        when(event) {
            is RulesViewModel.ScreenEvents.Navigate -> TODO("Add navigation to another screen")
            is RulesViewModel.ScreenEvents.ShowSnackbar -> 
                scaffoldState.snackbarHostState.showSnackbar(
                    message = "snackbar ${event.message}"
                )
        }
    }
}

Scaffold(
    scaffoldState = scaffoldState,
    snackbarHost = { snackbarHostState ->
        SnackbarHost(snackbarHostState) { data ->
            Snackbar(
                snackbarData = data
            )
        }
    }
) {
    TODO("Add your screen content")
}
like image 36
KolyaginVlad Avatar answered Oct 24 '25 05:10

KolyaginVlad



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!