Migrating from LiveData to Kotlin’s Flow
LiveData is something we need in 2017. Observer mode made our lives easier, but options like RxJava were too complex for beginners at the time. The Architecture Components team created LiveData: an observable data holder class designed specifically for Android. It is kept simple to get started, and it is recommended that RxJava be used for more complex reactive flow cases to take advantage of the integration between the two.
DeadData?
LiveData is still our solution for Java developers, beginners, and simple situations. For the rest, a good option is to turn to Kotlin Flows. Flows still have a steep learning curve, but they are part of the Kotlin language and are supported by Jetbrains; Compose is coming, and it’s perfect for the reactive model.
We have been talking about using Flows to connect different parts of the application, except the view and ViewModel. Now that we have a more secure way to collect streams from the Android UI, we can create a complete migration guide.
In this article, you’ll learn how to expose Flows to a view, how to collect them, and how to fine-tune them to meet specific requirements.
Simple things are harder, complex things are easier
LiveData does one thing and does it well: it exposes data while caching the latest values and understanding the Android life cycle.
Later we learned that it can also start coroutines and create complex transformations, but that’s a bit more complicated.
Let’s look at some LiveData schemas and their Flow equivalents:
#1: A data launch using the variable data holder
This is the classic pattern, where you can use the results of coroutines to change the state holder:
LiveData implementation
<! -- Copyright2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
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
}
}
}
Copy the code
The Flow to achieve
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
}
}
}
Copy the code
StateFlow is a special type of SharedFlow (it is a special type of Flow) that is closest to LiveData:
- There has been value
- There’s only one value
- Support for multiple observers
- It always sends the latest value of the subscription, regardless of the number of active observers.
Launch data
This is equivalent to the above behavior, but without creating mutable data.
LiveData implementation
class MyViewModel(...). : ViewModel() {val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
Copy the code
Since the state holder always has a value, it is best to wrap our UI state in some Result class that supports states such as load, success, and error.
The Flow to achieve
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
)
}
Copy the code
StateIn is the Flow operator that converts Flow to StateFlow. Let’s trust these parameters for now, because we’ll need more complicated manipulation later to interpret it properly.
A transmission of reference
Suppose you want to load some data that depends on the user ID, and you get this information from the Exposed stream’s AuthManager:
LiveData implementation
class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: LiveData<String? > = authManager.observeUser().map { user -> user.id }.asLiveData()val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
liveData { emit(repository.fetchItem(newUserId)) }
}
}
Copy the code
SwitchMap is a transformation whose body is executed and subscribes to the result when the userId changes.
If the userId has no reason to be a LiveData, a better alternative is to combine a Flow with a Flow and eventually convert the exposed results 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()
}
Copy the code
The Flow implementation is 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
)
}
Copy the code
Note that if you need more flexibility, you can also use transformLatest and issue the project explicitly:
valresult = userId.transformLatest { newUserId -> emit(Result.LoadingData) emit(repository.fetchItem(newUserId)) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000),
initialValue = Result.LoadingUser // Note the different Loading states
)
Copy the code
#4: Create a data stream that passes parameters
Now let’s make this example more reactive. Data is not captured, but observed, so we automatically propagate changes in the data source to the UI.
Continuing 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 streams to LiveData and issue all updates:
class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: LiveData<String? > = authManager.observeUser().map { user -> user.id }.asLiveData()val result = userId.switchMap { newUserId ->
repository.observeItem(newUserId).asLiveData()
}
}
Copy the code
Alternatively, it is better to combine the two streams using flatMapLatest and only convert the output to LiveData.
class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: Flow<String? > = authManager.observeUser().map { user -> user? .id }val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.asLiveData()
}
Copy the code
Implement with Flow
class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: 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
)
}
Copy the code
The exposed StateFlow receives updates every time the user changes or the user data in the repository changes.
#5 Combine multiple sources: MediatorLiveData -> flow.bine
MediatorLiveData allows you to observe one or more update sources (LiveData observables) and perform certain actions when they acquire new data. Normally, 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))}Copy the code
The Flow implementation is more straightforward
val flow1: Flow<Int> =...val flow2: Flow<Int> =...val result = combine(flow1, flow2) { a, b -> a + b }
Copy the code
You can also use the combineTransform function or zip.
Configuring exposed StateFlow (stateIn operator)
We used stateIn earlier to convert a regular flow to StateFlow, but it requires some configuration. If you don’t want to go into the details now, just copy and paste, I recommend this combination:
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
Copy the code
However, if you’re not sure about this seemingly random 5-second startup parameter, read on.
StateIn takes three parameters (from the documentation) :
@param
scope the coroutine scope in which sharing is started.
@param
started the strategy that controls when sharing is started and stopped.
@param
initialValue the initial value of the state flow.
This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with the `replayExpirationMillis` parameter.
Copy the code
Started can take three values
Lazily
: starts when the first subscriber appears, atscope
Stop on cancellation.Eagerly
Start at oncescope
Stop on cancellation.WhileSubscribed
: Complicated ***.***
For one-time operations, you can use Lazily or gbit/s. However, if you are looking at other processes, you should use WhileSubscribed to perform small but significant optimizations, as described below.
WhileSubscribed strategy
WhileSubscribed disconnects upstream streams when no collector is available. The StateFlow created using stateIn exposes data to the View, but it is also observing flows from other layers or applications (upstream). Keeping these flows active can result in a waste of resources, for example, if they continue to read data from other sources, such as database connections, hardware sensors, and so on. When your application goes into the background, you should be a good citizen and stop these coroutines.
WhileSubscribed has two parameters:
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
Copy the code
Stop timeout
From its documentation:
StopTimeoutMillis configures the delay, in milliseconds, between the disappearance of the last subscriber and the cessation of the upstream stream. It defaults to zero (stop immediately).
This is useful because you don’t want to cancel the upstream stream 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 rapid 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
)
}
Copy the code
This approach solves these problems:
- When the user exits your application into the background, updates from other layers stop after 5 seconds, saving battery life.
- The latest value is still cached so that when the user returns to it, the view immediately has some data.
- Subscribe to reboot and new values will appear, refreshing the screen when available.
Replay expiration
If you do not want users to see stale data after they have been away for too long and you prefer to display the load screen, look at the replayExpirationMillis parameter in WhileSubscribed. It’s handy in this case, and it saves some memory because the cached values revert to the initial values defined in stateIn. Returning to the application won’t be as fast, but you won’t display old data.
ReplayExpirationMillis – Configures the delay (in milliseconds) between a shared coroutine stop and a reslowdown memory reset (this makes the cache of the shareIn operator empty and resets the cached value to the original initial value of the stateIn operator). . It defaults to long. MAX_VALUE (always hold reslowing memory, never reset buffer). Use zero to immediately expire the cache.
Observing StateFlow from the view
So far, we have seen that it is important for views to let StateFlows in the ViewModel know that they are no longer listening. However, as with all things life cycle, it’s not that simple.
To collect flows, you need a coroutine. The Activity and coroutine builders provide a bunch of them:
-
Activity.lifecycleScope.launch
: Start the coroutine immediately and cancel it when the activity is destroyed.
-
Fragment.lifecycleScope.launch
: Starts the coroutine immediately and removes it when the fragment is destroyed.
-
Fragment.viewLifecycleOwner.lifecycleScope.launch
: Starts the coroutine immediately and cancels it when the view life cycle of the fragment is destroyed. If you are modifying the UI, you should use the view life cycle.
LaunchWhenStarted, launchWhenResumed…
A special version of launch call to launchWhenX will wait until lifecycleOwner is in the X state and suspend the coroutine when lifecycleOwner is below the X state. It is important to note that they do not remove coroutines until their lifecycle owners are destroyed.
Receiving updates while the application is in the background can cause a crash, which can be resolved by pausing the collection in the view. However, upstream flows remain active while the application is in the background, 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 a new API. # lifecycle. RepeatOnLifecycle to aid to this new coroutines builder (from the runtime life cycle - KTX 2.4.0 - alpha01) completely meet our needs: it started under the particular state coroutines, and owner below it stopped when they are in the life cycle.Copy the code
This will start collecting 'STARTED' when the Fragment view is, will continue through 'RESUMED', and stop 'STOPPED' when it comes back. In [from Android UI Collecting flow more] (https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda) in the safe way to read all the relevant information. ** Combine the * 'repeatOnLifecycle' *API with the above StateFlow guidelines to maximize device resources while achieving optimal performance. 支那Copy the code
Warning: Stateflow support, recently added to data binding using the launchWhenCreated collection update, will start using repeatOnLifecycle’ instead of when it reaches stable. For Data Binding, you should use Flows anywhere and simply add asLiveData() to expose them to the view. Data binding will be updated when Lifecycle – Run-time – KTX 2.4.0 stabilizes.
summary
The best way to expose data from the ViewModel and collect data from the view is to: - ✔️StateFlow exposes A using the WhileSubscribed strategy and times out. [Example] - ✔️ Collect repeatOnLifecycle. [Example] Any other combination would keep upstream Flows active, wasting resources: - use WhileSubscribed and collect inside lifecycleScope ❌ exposure. Launch/launchWhenX - ❌ use Lazily/public Eagerly and use repeatOnLifecycle, of course, If you don't need the full power of Flow... Just use LiveData. :)Copy the code