Collecting ViewState Outside LaunchedEffect

69 Views Asked by At

I have a query regarding Collecting ViewState outside LaunchedEffect when implementing MVI with Jetpack compose. I've noticed that when I collect the ViewModel's state outside a LaunchedEffect, it seems to work, but I'm concerned about potential issues like creating multiple collectors on recomposition. Can someone clarify if collecting ViewState outside a LaunchedEffect is safe or if it might lead to unintended behavior?

Is it recommended to use collectAsState() outside LaunchedEffect?

   @Composable
    fun ListScreenContent(callback: (countryName: String) -> Unit) {
        val viewModel: ListViewModel = hiltViewModel()
        val listViewState by viewModel.viewState.collectAsState()
    
        LaunchedEffect(Unit) {
            viewModel.sideEffect.collect {
                if (it is ListContract.SideEffect.NavigateToDetails) {
                    val countryName = it.countryName
                    callback(countryName)
                }
            }
        }
        ListViewStateComposable(viewState = listViewState, callback = { name ->
            ...
        })
      }

ViewModel

 val viewState: StateFlow<ListContract.ViewState>
    get() = _state.asStateFlow()

 val sideEffect: SharedFlow<ListContract.SideEffect>
    get() = _sideEffect.asSharedFlow()
1

There are 1 best solutions below

0
Thracian On BEST ANSWER
  val listViewState by viewModel.viewState.collectAsState()

This is call produceState which is

@Composable
fun <T : R, R> Flow<T>.collectAsState(
    initial: R,
    context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
    if (context == EmptyCoroutineContext) {
        collect { value = it }
    } else withContext(context) {
        collect { value = it }
    }
}

And produceState assigns value to State which is collected inside Flow.collect

@Composable
fun <T> produceState(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(key1, key2) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

Basically they are the same thing except when you implement logic inside collect in LaunchedEffect it's called only when you collect a new value but if you put

  if (it is ListContract.SideEffect.NavigateToDetails) {
                    val countryName = it.countryName
                    callback(countryName)
                }

this out of LaunchedEffect it will be called every time ListScreenContent is recomposed.