2
头图

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:

△ 将一次性操作的结果暴露给可变的数据容器 (LiveData)

△ 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):

△ 使用可变数据存储器 (StateFlow) 暴露一次性操作的结果

△ 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:

△ 把一次性操作的结果暴露出来 (LiveData)

△ 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 :

△ 把一次性操作的结果暴露出来 (StateFlow)

△ 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:

△ 带参数的一次性数据加载 (LiveData)

△ 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:

△ 带参数的一次性数据加载 (StateFlow)

△ 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.

△ 观察带参数的数据流 (LiveData)

△ 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:

△ 观察带参数的数据流 (StateFlow)

△ 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 by scope
  • Eagerly : Start immediately, and terminate when the scope specified by scope
  • 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.

△ 使用 launch/launchWhenX 来收集数据流是不安全的

△ 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.

△ 该 StateFlow 通过 WhileSubscribed(5000) 暴露并通过 repeatOnLifecycle(STARTED) 收集

△ 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 version repeatOnLifecyle .

For data binding , you should use Kotlin data flow everywhere and simply add asLiveData() to expose the data to the view. Data binding will be lifecycle-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 to StateFlow . [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 updates lifecycleScope.launch/launchWhenX
  • ❌ Expose StateFlow through the Lazily/Eagerly strategy, and collect data updates repeatOnLifecycle

Of course, if you don’t need to use the powerful features of Kotlin data streaming, just use LiveData :)

to Manuel , Wojtek , Yigit , Alex Cook, Florina and Chris


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。