RxJava: Too complicated to learn
LiveData: Customized for Android, easy to use
Flow: Simple things are harder, complex things are easier
Now let’s look at some LiveData schemas and their Flow equivalents:
1. Use mutable data holders to expose the results of one-time operations
This is the classic pattern, you can use the result of the coroutine to change the state holder:

Expose the result of a one-time operation using a mutable data holder (LiveData)
class MyViewModel {
private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
val myUiState: LiveData<Result<UiState>> = _myUiState
// Load data from a suspend fun and mutate state
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
Code language: PHP (php)
We can use StateFlow
to achieve the same effect:

Expose the result of a one-time operation using a mutable data holder (StateFlow)
class MyViewModel {
private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
val myUiState: StateFlow<Result<UiState>> = _myUiState
// Load data from a suspend fun and mutate state
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
Code language: PHP (php)
StateFlow
is a special kind of SharedFlow
(it’s a special type of Flow), closest to LiveData:
- always have value
- only one value
- Support for multiple subscribers
- Always replay the latest value of the subscription, regardless of the number of active observers
- Use StateFlow when exposing UI state to views. It is a safe and efficient observer designed to maintain the UI state.
2. Disclosing the results of a one-time operation
This is equivalent to the previous code snippet, exposing the result of a coroutine call without a mutable backing property.
For LiveData, we use the liveData coroutine builder for this:
Expose the result of a one-time operation (LiveData)

class MyViewModel(...) : ViewModel() { val result: LiveData<Result<UiState>> = liveData { emit(Result.Loading) emit(repository.fetchItem()) } }
Since the state holder always has a value, it’s better to wrap our UI state in some kind of Result class that supports states like loading, success, and errors.
Expose the result of a one-time operation (StateFlow)

class MyViewModel(...) : ViewModel() { val result: StateFlow<Result<UiState>> = flow { emit(repository.fetchItem()) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), // Or Lazily because it's a one-shot initialValue = Result.Loading ) }
stateIn
is the Flow operator that converts a Flow to a StateFlow. Let’s trust these parameters for now, as we’ll need more complexity later to interpret it correctly.
3. One-time data loading with parameters
Suppose you want to load some data that depends on the user ID, and you get this information from the AuthManager’s public flow:
One-time data loading with parameters (LiveData)

With LiveData you would do something like:
class MyViewModel(authManager..., repository...) : ViewModel() { private val userId: LiveData<String?> = authManager.observeUser().map { user -> user.id }.asLiveData() val result: LiveData<Result<Item>> = userId.switchMap { newUserId -> liveData { emit(repository.fetchItem(newUserId)) } } }
switchMap
is a transformation whose body is executed and subscribes to the result when userId
changes.
If there is no reason for userId to be LiveData, a better alternative is to combine Flow with Flow and finally convert the exposed result to LiveData.
class MyViewModel(authManager..., repository...) : ViewModel() { private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id } val result: LiveData<Result<Item>> = userId.mapLatest { newUserId -> repository.fetchItem(newUserId) }.asLiveData() }
One-time data loading with parameters (StateFlow)
Doing this with Flows looks very similar:

class MyViewModel(authManager..., repository...) : ViewModel() { private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id } val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId -> repository.fetchItem(newUserId) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading ) }
Note that if you need more flexibility, you can also use transformLatest
and emit data items explicitly:
val result = userId.transformLatest { newUserId ->
emit(Result.LoadingData)
emit(repository.fetchItem(newUserId))
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser // Note the different Loading states
)
Code language: JavaScript (javascript)
4. Observe the data flow with parameters
Now let’s make this more responsive example. The data is not fetched, but observed, so we automatically propagate changes in the data source to the UI.
Continuing with our example: instead of calling fetchItem
on the data source, we use a hypothetical observeItem operator that returns a Flow.
With LiveData, you can convert the stream to LiveData and emit all updates:
Observe a stream with parameters (LiveData)

class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()
val result = userId.switchMap { newUserId ->
repository.observeItem(newUserId).asLiveData()
}
}
Code language: HTML, XML (xml)
Alternatively, it’s better to use flatMapLatest
to combine the two streams and just convert the output to LiveData:
class MyViewModel(authManager..., repository...) : ViewModel() { private val userId: Flow<String?> = authManager.observeUser().map { user -> user?.id } val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId -> repository.observeItem(newUserId) }.asLiveData() }
Observe a flow with parameters (StateFlow)
Flow implements similar, but without the LiveData transformation:

class MyViewModel(authManager..., repository...) : ViewModel() { private val userId: Flow<String?> = authManager.observeUser().map { user -> user?.id } val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId -> repository.observeItem(newUserId) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.LoadingUser ) }
The exposed StateFlow receives updates whenever the user changes or the user data in the repository changes.
5. Combine multiple sources: MediatorLiveData -> Flow.combine
MediatorLiveData
lets you observe one or more update sources (LiveData observables) and perform certain actions when they get new data.
Typically, you update the value of MediatorLiveData:
val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...
val result = MediatorLiveData<Int>()
result.addSource(liveData1) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
result.addSource(liveData2) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
Code language: HTML, XML (xml)
The Flow equivalent is more straightforward:
val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }
Code language: HTML, XML (xml)
You can also use the combineTransform
function, or zip
.
Configure the exposed StateFlow (stateIn operator)
We previously used stateIn
to convert a regular flow to a StateFlow
, but it requires some configuration. If you don’t want to go into details right now, just copy and paste, I recommend this combination:
val result: StateFlow<Result<UiState>> = someFlow .stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading )
However, if you’re not sure about this seemingly random 5-second startup parameter, read on.
stateIn
has 3 parameters (from documentation):
- @param scope starts the shared coroutine scope.
- @param starts the strategy that controls when sharing starts and stops.
- @param initialValue The initial value of the state stream.
This value is also used when the state flow is reset using theSharingStarted.WhileSubscribed
strategy with thereplayExpirationMillisparameter
.
Can take 3 values to start with
- Lazily: starts when the first subscriber appears and stops when the scope is cancelled.+
- Eagerly: starts immediately and stops when the scope is canceled
- WhileSubscribed: This is complicated.
For one-time operations, you can useLazily
orEagerly
. However, if you are observing other processes, you should useWhileSubscribed
to perform small but important optimizations, as described below.
WhileSubscribed Policy
WhileSubscribed
cancels the upstream stream when there is no collector. A StateFlow
created with stateIn
exposes data to the View, but it is also observing flows from other layers or applications (upstream).
Keeping these streams active can lead to wasted resources, for example, if they continue to read data from other sources (such as database connections, hardware sensors, etc.).
WhileSubscribed
has two parameters:
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
Code language: PHP (php)
Stop timeout
From its documentation:
stopTimeoutMillisConfigure
the delay (in milliseconds) between the last subscriber disappearing and the upstream stream stopping. It defaults to zero (stop immediately).
This is useful because you don’t want to cancel the upstream flow if the view stops listening for a fraction of a second. This happens all the time – for example when the user rotates the device and the view is destroyed and recreated in quick succession.
The solution in the liveData coroutine builder is to add a 5-second delay, after which the coroutine will stop if there are no subscribers.WhileSubscribed(5000)
does exactly that:
class MyViewModel(...) : ViewModel() { val result = userId.mapLatest { newUserId -> repository.observeItem(newUserId) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading ) }
This method checks all boxes:
When the user sends your app to the background, updates from other tiers will stop after 5 seconds, saving power.
The latest value is still cached so that when the user comes back to it, the view has some data right away.
The subscription restarts, the new value will appear, refresh the screen when available.
replay expires
replayExpirationMillis
– Configure the delay (in milliseconds) between stopping the shared coroutine and resetting the replay cache (this makes the shareIn operator’s cache empty and resets the cache value to the stateIn operator’s original initial value). It defaults to Long.MAX_VALUE
(keep the replay cache forever, never reset the buffer). Use a value of zero to immediately expire the cache.
Observe StateFlow from view
So far, we’ve seen that it’s important to let the StateFlows
in the ViewModel
know that the View is no longer listening. However, as with everything related to the lifecycle, things are not that simple.
To collect the stream, you need a coroutine. Activities and Fragments provide a bunch of coroutine builders:
- Activity.lifecycleScope.launch:Start the coroutine immediately and cancel the coroutine when the activity is destroyed.
- Fragment.lifecycleScope.launch:Start the coroutine immediately, and cancel the coroutine when the fragment is destroyed.
- Fragment.viewLifecycleOwner.lifecycleScope.launch:Start the coroutine immediately, and cancel the coroutine when the fragment’s view lifetime is destroyed. If you are modifying the UI, you should use the view lifecycle.
LaunchWhenStarted, launchWhenResumed
A special version of launch called launchWhenX
will wait until the lifecycleOwner
is in the X state and suspend the coroutine when the lifecycleOwner
is below the X state

It is not safe to use "launch/launchWhenX"
to collect streams
Receiving updates while the app is in the background can cause a crash, which can be resolved by pausing the collection in the view. However, when the app is in the background, the upstream stream remains active, which can be a waste of resources.
This means that everything we’ve done so far to configure StateFlow will be useless; however, there is now a new API.
lifecycle.repeatOnLifecycle
This new coroutine builder (available from lifecycle-runtime-ktx 2.4.0-alpha01 ) is exactly what we need: it starts coroutines in a specific state and stops them when the lifecycle owner falls below it.
Different traffic collection methods
For example, in a Fragment:
onCreateView(...) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) { myViewModel.myUiState.collect { ... } } } }
This will start collecting when the Fragment’s view starts, will continue through RESUMED
, and stop when it returns to STOPPED
.
Click to read the full introduction about A safer way to collect flows from Android UIs .
Combine the repeatOnLifecycle
API with the StateFlow guidelines above to get the best performance while making full use of device resources.
StateFlow is exposed using WhileSubscribed(5000)
and collected using repeatOnLifecycle(STARTED)
Warning: StateFlow support recently added to Data Binding is currently used launchWhenCreated
to collect updates and will be adopted when stable repeatOnLifecycle
For data binding, you should use Flows and simply add asLiveData()
to expose them to the view.
Summarize
The best way to expose data from ViewModel
and collect data from View is:
- Expose with a
WhileSubscribedpolicy
StateFlowand set a timeout - Collect using
repeatOnLifecycle
Any other combination would keep upstream Flows alive, wasting resources: - Use
WhileSubscribedexpose
and use thelaunch/launchWhenXcollection
within the scope of the lifecycle - use
Lazily/Eagerlypublic
and userepeatOnLifecyclecollect