LiveData dates back to 2017. At the time, the observer pattern effectively simplified development, but libraries such as RxJava were a bit too complicated for novices. To this end, the architecture component team created LiveData : an observable data storage class dedicated to Android with autonomous life cycle awareness. LiveData is designed to simplify the design, which makes it easy for developers to get started; for more complex interactive data flow scenarios, it is recommended that you use RxJava, so that the advantages of the combination of the two can be brought into play.
DeadData?
LiveData for Java developers, beginners or some simple scene concerned is still viable solution . For some other scenarios, a better choice is to use Kotlin Flow (Kotlin Flow) . Although data flow (compared to LiveData) has a steeper learning curve, since it is part of the Kotlin language supported by JetBrains, and the official version of Jetpack Compose is about to be released, the two can work together to better play the responsiveness of Kotlin data flow The potential of the model.
Some time ago, we discussed how can use Kotlin data stream to connect other parts of your application except the view and the view model. And now we have a safer way to get the data stream from the Android interface, and we can create a complete migration guide.
In this article, you will learn how to expose data streams to views, how to collect data streams, and how to adapt to different needs through tuning.
Data Stream: To make simple and complex, and to make complex simple
LiveData to do one thing and do it well: it cache the latest data and Android simultaneously perceive the life cycle of the data exposed. Later we will learn LiveData can also start Coroutine and create complex data conversion , this may take some time.
Next, let's compare the corresponding writing in LiveData and Kotlin data stream together:
#1: Use variable data memory to expose the result of a one-time operation
This is a classic mode of operation, where you use the results of the coroutine to change the state container:
△ Expose the results of a one-time operation to a variable data container (LiveData)
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class MyViewModel {
private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
val myUiState: LiveData<Result<UiState>> = _myUiState
// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
If we want to perform the same operation in Kotlin data flow, we need to use (variable) StateFlow (state container observable data flow):
△ Use variable data memory (StateFlow) to expose the results of one-time operations
class MyViewModel {
private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
val myUiState: StateFlow<Result<UiState>> = _myUiState
// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
StateFlow is SharedFlow , and SharedFlow is a special type of data flow of Kotlin. StateFlow and LiveData are the closest, because:
- It is always valuable.
- Its value is unique.
- It allows to be shared by multiple observers (hence a shared data stream).
- It will always only reproduce the latest value to subscribers, which has nothing to do with the number of active observers.
When exposing the state of the UI to the view, StateFlow should be used. This is a safe and efficient observer, specifically designed to accommodate UI state.
#2: Expose the results of a one-time operation
This example is consistent with the effect of the above code snippet, except that the result of the coroutine call is exposed here without using variable attributes.
If using LiveData, we need to use LiveData coroutine builder:
△ Expose the results of a one-time operation (LiveData)
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
Since the state container always has a value, then we can Result class, such as loading, success, error and other states.
The corresponding data flow method requires you to do a bit more configuration :
△ Expose the results of a one-time operation (StateFlow)
class MyViewModel(...) : ViewModel() {
val result: StateFlow<Result<UiState>> = flow {
emit(repository.fetchItem())
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000), //由于是一次性操作,也可以使用 Lazily
initialValue = Result.Loading
)
}
stateIn is an operator that specifically converts data flow to StateFlow. Since more complex examples are needed to explain it better, these parameters are left aside for the time being.
#3: One-time data load with parameters
For example, you want to load some data that depends on the user ID, and the information comes from an AuthManager that provides data flow:
△ One-time data loading with parameters (LiveData)
When using LiveData, you can use code like this:
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 kind of data transformation. It subscribes to the change of userId, and its code body will be executed when it senses the change of userId.
If it is not necessary to use userId
as LiveData, a better solution is to combine streaming data with Flow, and convert the final result (result) into 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()
}
If you use Kotlin Flow to write, the code actually looks familiar:
△ One-time data loading with parameters (StateFlow)
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
)
}
If you want more flexibility, you can consider calling the transformLatest and emit methods explicitly:
val result = userId.transformLatest { newUserId ->
emit(Result.LoadingData)
emit(repository.fetchItem(newUserId))
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser //注意此处不同的加载状态
)
#4: Observe the data stream with parameters
Next we make the case just now more interactive. The data is no longer read, but is observed , so our changes to the data source will be directly transferred to the UI interface.
Continuing the previous example: we no longer call the fetchItem method on the source data, but get a Kotlin data stream through the assumed observeItem method.
If you use LiveData, you can convert the data stream to a LiveData instance, and then transmit the data changes through emitSource.
△ Observe the data 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()
}
}
Or in a more recommended way, combine the two streams through flatMapLatest , and only convert the final 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()
}
The implementation of using Kotlin data stream is very similar, but it saves the conversion process of LiveData:
△ Observe the data flow with parameters (StateFlow)
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
)
}
Whenever the user instance changes or the user's data in the repository changes, the StateFlow exposed in the above code will receive the corresponding update information.
#5: Combine multiple sources: MediatorLiveData -> Flow.combine
MediatorLiveData allows you to observe the changes of one or more data sources and perform corresponding operations based on the new data obtained. The value of MediatorLiveData can usually be updated in the following way:
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))
}
The same function is more straightforward to operate using Kotlin data flow:
val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }
combineTransform or zip function can also be used here.
StateFlow exposed through stateIn configuration
Earlier, we used the stateIn
intermediate operator to convert the ordinary flow into StateFlow, but some configuration work is required after the conversion. If you don't want to know too many details now, but just want to know how to use it, then you can use the following recommended configuration:
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
However, if you want to know why this seemingly random 5-second started parameter is used, please read on.
According to the document, stateIn
has three parameters:
@param scope 共享开始时所在的协程作用域范围
@param started 控制共享的开始和结束的策略
@param initialValue 状态流的初始值
当使用 [SharingStarted.WhileSubscribed] 并带有 `replayExpirationMillis` 参数重置状态流时,也会用到 initialValue。
started
accepts the following three values:
Lazily
: Start when the first subscriber appears, and terminate when the scope specified byscope
Eagerly
: Start immediately, and terminate when the scope specified byscope
WhileSubscribed
: This situation is a bit complicated (more on this later).
For operations that are only performed once, you can use Lazily or Eagerly. However, if you need to observe other streams, you should use WhileSubscribed to achieve subtle but important optimization work, see the answer later.
WhileSubscribed strategy
The WhileSubscribed strategy will cancel the upstream data stream . The StateFlow created by the stateIn operator exposes the data to the view, and also observes the data flow from other layers or upstream applications. Keeping these streams active may cause unnecessary waste of resources, such as always reading data from database connections, hardware sensors, and so on. When your application turns to run in the background, you should exercise restraint and stop these coroutines .
WhileSubscribed
accepts two parameters:
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
timeout stop
According to its documentation:
stopTimeoutMillis controls a delay value in milliseconds, which refers to the time difference between the last subscriber to end the subscription and stop the upstream stream. The default value is 0 (stop immediately).
This value is very useful because you may not want to end the upstream stream just because the view is not listening for a few seconds. This situation is very common-for example, when the user rotates the device, the original view will be destroyed first, and then rebuilt within a few seconds.
The method used by the liveData coroutine builder is add a 5-second delay , that is, if there are no subscribers after waiting for 5 seconds, the coroutine will be terminated. The WhileSubscribed (5000) in the preceding code implements this function:
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
This method will be reflected in the following scenarios:
- The user transfers your application to the background to run, and all data updates from other layers will stop after 5 seconds, which can save power.
- The latest data will still be cached, so when the user switches back to the application, the view can immediately get the data for rendering.
- The subscription will be restarted, new data will be populated, and the view will be updated when data is available.
Data reproduction expiration time
If the user has been away from the application for too long, at this time you do not want the user to see the stale data, and you want to show that the data is being loaded, then you should use the replayExpirationMillis parameter in the WhileSubscribed strategy. In this case, this parameter is very suitable, because the cached data is restored to the initial value defined in stateIn, it can effectively save memory. Although the user may not display valid data so quickly when switching back to the application, at least the expired information will not be displayed.
replayExpirationMillis
configures a delay time in milliseconds, which defines the waiting time from stopping the shared coroutine to resetting the cache (recovering to the initial value defined in the stateIn operator). Its default value is the maximum value of the long integer Long.MAX_VALUE (meaning it will never be reset). If set to 0, the cached data can be reset immediately when the conditions are met.
from the view
As we have already talked about, the StateFlow in the ViewModel needs to know that they no longer need to listen. However, when all these things are combined with the lifecycle, things are not that simple.
To collect a data stream, you need to use a coroutine. Activity and Fragment provide several coroutine builders:
- Activity.lifecycleScope.launch : Start the coroutine immediately, and end the coroutine when the Activity is destroyed.
- Fragment.lifecycleScope.launch : Start the coroutine immediately, and end the coroutine when the Fragment is destroyed.
- Fragment.viewLifecycleOwner.lifecycleScope.launch : Start the coroutine immediately, and cancel the coroutine when the life cycle of the view in this Fragment ends.
LaunchWhenStarted and LaunchWhenResumed
For a state X, there is a special launch method called launchWhenX. It will wait until the lifecycleOwner enters the X state, and then suspends the coroutine when it leaves the X state. , it should be noted that the coroutine corresponding to 160de813b3da72 will only be cancelled when their life cycle owner is destroyed.
△ It is not safe to use launch/launchWhenX to collect data streams
Receiving data updates while the app is running in the background may cause the app to crash, but this situation can be solved by suspending the view's data flow collection operation. However, the upstream data stream will remain active while the application is running in the background, so some resources may be wasted.
In this way, the current configuration of StateFlow is useless; however, there is now a new API.
lifecycle.repeatOnLifecycle Come to the rescue
This new coroutine builder (available since lifecycle-runtime-ktx 2.4.0-alpha01 ) just meets our needs: start the coroutine when a certain state is met, and exit when the lifecycle owner exits Stop the coroutine in this state.
△ Comparison of different data stream collection methods
For example, in the code of a Fragment:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
When this Fragment is in the STARTED state, it will start to collect the stream, and keep collecting in the RESUMED state, and finally end the collection process when the Fragment enters the STOPPED state. For more information, please refer to: Use a safer way to collect Android UI data stream .
combined with repeatOnLifecycle API and the above StateFlow example can help your application to make good use of device resources and at the same time give full play to the best performance.
△ The StateFlow is exposed through WhileSubscribed(5000) and collected through repeatOnLifecycle(STARTED)
Note : recent the Data the Binding added in StateFlow support used
launchWhenCreated
to describe the collection of data update, and it will in turn be used after entering the stable versionrepeatOnLifecyle
.For data binding , you should use Kotlin data flow everywhere and simply add
asLiveData()
to expose the data to the view. Data binding will belifecycle-runtime-ktx 2.4.0
enters the stable version.
summary
The best way to expose data through the ViewModel and get it in the view is:
WhileSubscribed
strategy with timeout parameters toStateFlow
. [Example 1 ]- ✔️ Use
repeatOnLifecycle
to collect data updates. [Example 2 ]
If other methods are used, the upstream data stream will be kept active all the time, resulting in a waste of resources:
- ❌ Expose StateFlow through
WhileSubscribed
, and then collect data updateslifecycleScope.launch/launchWhenX
- ❌ Expose StateFlow through the
Lazily/Eagerly
strategy, and collect data updatesrepeatOnLifecycle
Of course, if you don’t need to use the powerful features of Kotlin data streaming, just use LiveData :)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。