PoC using Flow to the View on an Android Project. DO NOT expose a Flow to the View. Use LiveData to communicate between ViewModel and View instead.
TL;DR :
Flow with .asLiveData() or liveData {} |
Flow with .stateIn() |
viewModelScope.launch() with MutableLiveData field |
|
---|---|---|---|
ViewModelScope | ✔️ | ✔️ | ✔️ |
Lifecycle | ✔️ Suspended after Activity is stopped for 5s (default) |
✔️ Suspended after SharingStarted 's timeout |
❌ Resources will be wasted (but no crash) as soon as Activity is stopped |
Republish content on rebind | ✔️ No | ❌ Yes (need distinctUntilChanged() before collect on View) |
✔️ No |
Adjustable Timeout | ✔️timeoutInMs parameter |
✔️stopTimeoutMillis of SharingStarted.WhileSubscribed value for started parameter |
❌ |
On timeout (5s) behavior | Restarts only if Flow didn't complete¹ | Restarts only if Flow didn't complete¹ or always exposes initial state and restarts Flow (with replayExpirationMillis = 0 ) |
❌ |
Adjustable Initial state | ✔️ Must expose initial state manually (can use latestValue to check if a previous value has been emitted) |
✔️✔️ Initial state is exposed automatically when coroutine is restarted |
❌ |
Dynamic Initial state | ✔️ Can change arbitrarily |
❌ Initial state can't change between timeouts |
❌ |
Unit Testing | ✔️ Must inject Dispatchers Must use TestCoroutineScope & InstantTaskExecutorRule |
✔️ Must inject Dispatchers and SharingStarted strategyMust use TestCoroutineScope |
✔️ Must inject Dispatchers Must use TestCoroutineScope & InstantTaskExecutorRule |
Boilerplate | ✔️✔️ 2 lines: field .asLiveData(dispatchers.io) or liveData(dispatchers.io) {} injected Dispatchers |
❌ 4 to 6 lines: .stateIn() shared parameter scope parameter initial value parameter injected Dispatchers injected SharingStarted strategy |
❌ 4 to 5 lines: private MutableLiveData field LiveData getter init {} viewModelScope.launch injected Dispatchers |
Complexity / Error prone | ✔️ | ❌ | ✔️ |
LiveData dependency | ❌ | ✔️ | ❌ |
¹ : If a hot flow (MutableStateFlow, MutableSharedFlow, Channel, etc...) is used to produce values downstream, the flow will never complete !
This means the collect
will restart after the 5s timeout, even if it would seem unnecessary.
More information here
Using .asLiveData()
extension, liveData()
function or .stateIn()
extension is a better approach than simply launching on the viewModelScope
to publish to a MutableLiveData
or MutableStateFlow
. This is because when using .asLiveData()
, liveData()
or stateIn()
, the coroutine is cancelled 5 seconds after leaving the Activity (not killing the application, just pressing 'home' or 'recent apps' button !), avoiding possibly unnecessary work.
There is one way to go with pure Kotlin, meaning there's no need for LiveData dependencies, but I wouldn't recommend it at the time, because any hot flow (see ¹) will make your collection restart after a timeout. Using a MutableLiveData
with a switchMap()
operator to "trigger" a flow is the only viable option to this day. Also, unit testing is a bit more complicated (one needs to inject both Dispatchers
and SharingStarted
strategy). And, to my opinion, it's more error prone.
See for yourself in the project, both "pure Kotlin" and LiveData approach are used !
Inspired by : https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda