Explore different ways of converting Flow to SharedFlow and StateFlow using SharedFlow.emit(), StateFlow.value, Flow.ShareIn() and Flow.StateIn()
This is part of the asynchronous flow series:
Part 4 - Convert Flow to SharedFlow and StateFlow
Flow
is a cold stream. It emits value only when someone collects or subscribes to it. So it does NOT hold any data or state.
SharedFlow
is a hot stream. It can emit value even if no one collects or subscribes to it. It does NOT hold any data too.
StateFlow
is also a hot steam. It does NOT emit value, but it holds the value/data.
Flow Type | Cold or Hot Stream | Data Holder |
---|---|---|
Flow | Cold | No |
SharedFlow | Hot (by default) | No |
StateFlow | Hot (by default) | Yes |
The reason why
SharedFlow
andStateFlow
are hot streams by default, is they can also be cold streams depending on how you create them. See shareIn and stateIn sections below.
What is data holder?
Data holder (can also be called as state holder) means it holds data. It retains and stores the last data of the stream, The data is also observable which allows subscribers to subscribe to it.
There are 3 types of data holders in Android development
LiveData
StateFlow
State
(Jetpack Compose)
3 of them are pretty similar, but they have differences. See the table below.
Data Holder Type | Android or Kotlin Library? | Lifecycle Aware? | Required Initial Value? |
---|---|---|---|
LiveData | Android | Yes | No |
StateFlow | Kotlin | No | Yes |
State (Compose) | Android | No | Yes |
StateFlow is Platform Independent
LiveData
is Android-specific and eventually will be replaced by StateFlow
. Compose state
is similar to StateFlow
in my opinion. However, compose State
is very specific to Android Jetpack Compose. So it is platform specific, whereas StateFlow
is more generic and platform independent.
StateFlow could be life-cycle aware
LiveData
itself is life-cycle aware and StateFlow
is NOT. StateFlow
could be life-cycle aware, depending on how you collect it. Compose State
itself is NOT life-cycle aware. Since it is used by the composable functions, when a composable function leaves composition, it automatically unsubscribes from compose State
.
StateFlow requires Initial Value
Creating LiveData
does NOT require an initial value
// live data - data holder
val liveData = MutableLiveData<Int>()
but, StateFlow
and compose State
require an initial value.
// state flow - data holder
val stateFlow = MutableStateFlow<Int?>(null)
// compose state - data holder
val composeState: MutableState<Int?> = mutableStateOf(null)
Convert Flow to SharedFlow
The following example is based on this flow in your View Model class
val flow: Flow<Int> = flow {
repeat(10000) { value ->
delay(1000)
emit(value)
}
}
and this sharedFlow
variable defined.
private var sharedFlow = MutableSharedFlow<Int>()
1. Flow.collect() and SharedFlow.emit()
This converts the Flow
to SharedFlow
using Flow<T>.collect
and manually call the SharedFlow<T>.emit()
.
viewModelScope.launch {
flow.collect { value
-> sharedFlow.emit(value)
}
}
This is a hot stream. So it emits the value regardless anyone collects it.
2. Flow.shareIn()
You can also use Flow<T>.shareIn()
to achieve the same result.
sharedFlow = flow.shareIn(
scope = viewModelScope,
started = SharingStarted.Eagerly
)
However, if you change SharingStarted.Eagerly
to SharingStarted.WhileSubscribed()
, the SharedFlow
becomes a cold stream.
Convert Flow to StateFlow
So we have this stateFlow
variable defined in the view model.
private val stateFlow = MutableStateFlow<Int?>(null)
1. Flow.collect() and StateFlow.value
This converts the Flow
to StateFlow
.
viewModelScope.launch {
flow.collect { value ->
stateFlow.value = value
}
}
Similar to SharedFlow
, this is a hot stream. The difference is StateFlow
is a data holder and SharedFlow
is not.
2. Flow.stateIn()
You can also use Flow<T>.stateIn()
to achieve the same result.
stateFlow = flow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null)
Similar to Flow<T>.shareIn()
above, if you change SharingStarted.Eagerly
to SharingStarted.WhileSubscribed()
, the StateFlow
becomes a cold stream.
In practice, it is advisable to use SharingStarted.WhileSubscribed(5000)
instead of SharingStarted.WhileSubscribed()
to account for screen rotation and prevent flow emission from restarting.
Important note: If you call
Flow<T>.shareIn()
andFlow<T>.stateIn()
multiple times, it creates multiple flows which emit the value in the background. This eventually causes unnecessary resource leaks that you want to prevent.
Collect from SharedFlow and StateFlow
Collecting from SharedFlow
and StateFlow
is the same as collecting from the Flow
. Refer to the following article on different ways of collecting flow.
1. Collect using RepeatOnLifecycle()
The recommended way to collect Flow
by Google is using LifeCycle.RepeatOnLifecycle()
. So, we're going to use it as an example.
This example below converts the stateFlow
to compose state, which is the data holder for composable function.
@Composable
fun SharedStateFlowScreen() {
val viewModel: StateSharedFlowViewModel = viewModel()
val lifeCycle = LocalLifecycleOwner.current.lifecycle
/* compose state - data holder */
var composeStateValue by remember { mutableStateOf<Int?>(null) }
/* collect state flow and convert the data to compose state */
LaunchedEffect(true) {
lifeCycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
viewModel.stateFlow.collect { composeStateValue = it }
}
}
}
2. Collect using collectAsStateWithLifecycle()
If you use Android lifecycle Version 2.6.0-alpha01 or later, you can reduce the code significantly using Flow<T>.collectAsStateWithLifecycle()
API
First, you need to have this dependency in your build.gradle
file.
dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha02'
}
Then, you can reduce the code to the following. Please note that you need to specify the @OptIn(ExperimentalLifecycleComposeApi::class)
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun SharedStateFlowScreen() {
val viewModel: StateSharedFlowViewModel = viewModel()
/* compose state - data holder */
val composeStateValue by
viewModel.stateFlow.collectAsStateWithLifecycle()
}
Best Practices?
Honestly, what I find difficult in Android development is there are just way too many options to accomplish the same thing. So which one we should use? Now, we have LiveData
, Flow
, Channel
, SharedFlow
, StateFlow
and compose State
. In what scenario, we should use which one?
So I document the best practices based on various sources and my interpretations. I do not know whether they make sense. Things like this are likely very subjective too.
Do NOT use LiveData especially if you're working on a new project.
LiveData
is legacy and eventually will be replaced byStateFlow
.Do NOT expose Flow directly in View Model, convert it to
StateFlow
instead. This can avoid unnecessary workload on the main UI thread.Flow
is a cold stream, it emits data (or restarts the data emission) every time you collect it.Expose StateFlow in your view model instead of compose
State
. SinceStateFlow
is platform independent, it makes your view model platform independent which allows you easy migration to KMM (which allows you to target both IOS and Android) for example.StateFlow
is also more powerful (e.g. it allows you to combine multiple flows into one etc.)Collect StateFlow in your UI elements (either in activity or composable function) and convert the data to compose
State
. ComposeState
should be created and used only within the composable functions.
Whether ViewModel should hold
StateFlow
or composeState
is questionable. I have been using composeState
but it seems likeStateFlow
might be a better option here based on more complex use cases such as combining flow?On the other hand, if I convert
Flow
to composeState
in ViewModel directly, I don't need to convert it again in the composable function. Why do I need 2 state/data holders and collect twice?
What about one-time event?
I do not sure of the use case of SharedFlow
and Channel
. It appears to be used as a one-time event. If you have one subscriber, you use Channel
. If you have multiple subscribers, you use SharedFlow
.
However, this article here by the Google team kind of imply using SharedFlow
or Channel
as a one-time event is not recommended. It is mainly because they're hot stream, which runs into a risk of missing the events when the app is in the background or during configuration.
It seems to me we should probably just use StateFlow for everything. Let's forget the rest! Maybe this is easier this way...
Source Code
GitHub Repository: Demo_AsyncFlow (see the SharedStateFlowActivity
)
Originally published at https://vtsen.hashnode.dev.
Top comments (0)