I'm trying to follow the official guidelines to migrate from LiveData to Flow/StateFlow with Compose, as per these articles:
A safer way to collect flows from Android UIs
Migrating from LiveData to Kotlin’s Flow
I am trying to follow what is recommended in the first article, in the Safe Flow collection in Jetpack Compose section near the end.
In Compose, side effects must be performed in a controlled environment. For that, use LaunchedEffect to create a coroutine that follows the composable’s lifecycle. In its block, you could call the suspend Lifecycle.repeatOnLifecycle if you need it to re-launch a block of code when the host lifecycle is in a certain State.
I have managed to use .flowWithLifecycle() in this way to make sure the flow is not emmiting when the app goes to the background:
@Composable
fun MyScreen() {
val lifecycleOwner = LocalLifecycleOwner.current
val someState = remember(viewModel.someFlow, lifecycleOwner) {
viewModel.someFlow
.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.stateIn(
scope = viewModel.viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
}.collectAsState()
}
I find this very "boilerplatey" -there must be something better. I would like to have StateFlow in the ViewModel, instead of Flow that gets converted to StateFLow in the @Composable, and use .repeatOnLifeCycle(), so I can use multiple .collectAsState() with less boilerplate.
When I try to use .collectAsState() inside a coroutine (LaunchedEffect), I obviously get an error about .collectAsState() having to be called from the context of @Composable function.
How can I achieve similar functionality as with .collectAsState(), but inside .repeatOnLifecycle(). Do I have to use .collect() on the StateFlow and then wrap the value with State? Isn't there anything with less boilerplate than that?
After reading a few more articles, including
Things to know about Flow’s shareIn and stateIn operators
repeatOnLifecycle API design story
I am becoming more convinced of using .flowWithLifecycle() in this way instead of repeatOnLifecycle() + produceState(). I just made this extension function to alleviate the boilerplate when collecting a flow as State from a @Composable:
@Composable
fun <T> Flow<T>.flowWithLifecycleStateInAndCollectAsState(
scope: CoroutineScope,
initial: T? = null,
context: CoroutineContext = EmptyCoroutineContext,
): State<T?> {
val lifecycleOwner = LocalLifecycleOwner.current
return remember(this, lifecycleOwner) {
this
.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = initial
)
}.collectAsState(context)
}
This would then be used like this in a @Composable:
@Composable
fun SomeScreen() {
//...
val someState = viewModel.someFlow
.flowWithLifecycleStateInAndCollectAsState(
scope = viewModel.viewModelScope //or the composable's scope
)
//...
}
I will mark this as accepted solution in case it helps someone else, unless someone lets me know of a better way in the next few days.
Building upon OP's answer, it can be a bit more light-weight by not going through StateFlow internally, if you don't care about the WhileSubscribed(5000) behavior.
@Composable
fun <T> Flow<T>.toStateWhenStarted(initialValue: T): State<T> {
val lifecycleOwner = LocalLifecycleOwner.current
return produceState(initialValue = initialValue, this, lifecycleOwner) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect { value = it }
}
}
}