原文链接

LiveData ,是咱们退回到 2017 年才需求的东西。调查者形式,确实简化了咱们的工作方法,但 RxJava 等选项,关于当时的初学者来说实在是太杂乱了。因而 Architecture Components 团队创立了LiveData :这是个十分 “有主见的” 可调查数据持有者类,而且是专门为 Android 设计的。它坚持简略明了,这让它易于上手,主张是将 RxJava 用于更杂乱的 呼应流 案例,以充分利用这两者之间的整合。

死数据?

LiveData 依然是咱们 针对 Java 开发人员、初学者和简略情况的解决计划。关于其余部分,一个不错的选择是搬迁到 Kotlin Flows。Flows 依然有一个陡峭的学习曲线,但它们是 Kotlin 语言的一部分,由 Jetbrains 供给支撑;Compose 即将到来(已到来),它十分适合呼应式模型。

一段时刻以来,咱们一直在评论运用 Flows 连接 app 的不同部分,但 view 和 ViewModel 在外。现在咱们有了更安全的办法从 Android UI 搜集 flows,咱们能够创立一个完整的搬迁攻略。

在这篇文章中,您将学习如何将 Flows 暴露给view、如何搜集它们,以及如何对其进行微调,以满足特定需求。

Flow:简略的工作更难了,杂乱的工作却更容易

LiveData 做了一件很漂亮的事儿:它 揭露数据,一起缓存最新值,并知晓 Android 的生命周期。后来咱们了解到它也能够 发动协程,并 创立杂乱的转化,但这就有点杂乱了。

让咱们看一些 LiveData 形式及其 Flow 等效代码:

#1: 运用 Mutable(可变)数据持有者,揭露 一次性操作 的成果

这是经典形式,在这种形式中,你能用 协程的成果 来改动 状况持有者:

从 LiveData 迁移到 Kotlin 的 Flow

运用 Mutable(可变)数据持有者 (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
    // 从 suspend fun 加载数据并改动状况
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

为了对 Flows 履行相同的操作,咱们运用(Mutable 可变的)StateFlow:

从 LiveData 迁移到 Kotlin 的 Flow

运用 可变数据容器(StateFlow),揭露 一次性操作 的成果

class MyViewModel {
    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
    val myUiState: StateFlow<Result<UiState>> = _myUiState
    // 从 suspend fun 加载数据并改动状况
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

StateFlow是一种特殊类型的SharedFlow(这是一种特定类型的 Flow),最接近 LiveData:

  • 它总有一个值。
  • 它只要一个值。
  • 它支撑多个调查者(因而 flow 是 同享的)。
  • 它总是在订阅时,replay 最新的值,与 活泼调查者 的数量无关。

向 view 揭露 UI 状况时,请运用 StateFlow。它是一个安全高效的调查者,旨在持有 UI 状况。

#2: 揭露 一次性操作 的成果

这等效于前面的代码段,在没有可变的 后备特点 的情况下,揭露协程调用的成果。

关于 LiveData,咱们运用了 liveData 协程 builder:

从 LiveData 迁移到 Kotlin 的 Flow

揭露 一次性操作 的成果(LiveData)

class MyViewModel(...) : ViewModel() {
    val result: LiveData<Result<UiState>> = liveData {
        emit(Result.Loading)
        emit(repository.fetchItem())
    }
}

由于状况持有者总是有一个值,所以最好将 UI状况 封装在某种支撑 LoadingSuccessError 等状况的Result类中。

由于必须进行一些 装备,因而等效的 Flow 代码触及的内容会更多:

从 LiveData 迁移到 Kotlin 的 Flow

揭露 一次性操作 的成果 (StateFlow)

class MyViewModel(...) : ViewModel() {
    val result: StateFlow<Result<UiState>> = flow {
        emit(repository.fetchItem())
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), // 或许 Lazily,由于它是一次性的
        initialValue = Result.Loading
    )
}

stateIn 是一个 Flow 运算符,它将 Flow 转化为 StateFlow。让咱们暂时完全信赖这些参数,由于咱们需求更多的杂乱性(常识),才能在以后正确解说它。

#3: 带参的 一次性数据 加载

假定,你想加载一些依赖于用户 ID 的数据,而且你从揭露 Flow 的 AuthManager 取得这些信息:

从 LiveData 迁移到 Kotlin 的 Flow

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

运用LiveData,您能够履行类似的操作:

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是一个转化,当 userId 改动时,它的主体将被履行,一起成果也会被订阅。

如果没理由让 userId 成为 LiveData,那么更好的代替计划是将 streams 与 Flow 结合起来,并最终将揭露的成果,转化为 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()
}

运用 Flows 履行此操作,看起来十分类似:

从 LiveData 迁移到 Kotlin 的 Flow

带参的 一次性数据 加载 (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
    )
}

请留意,如果您需求更大的灵活性,也能够运用 transformLatest 并显式地 emit(发射) 条目:

val result = userId.transformLatest { newUserId ->
        emit(Result.LoadingData)
        emit(repository.fetchItem(newUserId))
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser // 留意不同的 Loading 状况
    )

#4: 调查带有参数的数据流(stream)

现在让咱们让这个比如,更具 呼应性。数据不是被获取的,而是 被调查的,因而咱们将数据源中的更改,主动传播到 UI。

继续咱们的示例:咱们不在数据源上调用 fetchItem,而是运用一个假定的observeItem 运算符来回来 Flow。

运用 LiveData,您能够将流通化为 LiveData,并 emitSource一切更新:

从 LiveData 迁移到 Kotlin 的 Flow

调查带有参数的 stream(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()
    }
}

或许,最好运用 flatMapLatest 组合两个 flow,并仅将输出转化为 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()
}

Flow 实现是类似的,但没有 LiveData 转化:

从 LiveData 迁移到 Kotlin 的 Flow

调查带有参数的 stream(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
    )
}

每逢用户更改,或存储库中的用户数据更改时,揭露的 StateFlow 都将收到更新。

#5 合并多个来源:MediatorLiveData -> Flow.combine

MediatorLiveData 让您能够调查一个或多个更新源(LiveData 可调查目标)并在它们取得新数据时做一些工作。 一般,您会更新 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))
}

Flow 等效代码,要简略得多:

val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }

您还能够运用combineTransform函数或 zip。

装备揭露的 StateFlow(stateIn 运算符)

咱们之前运用 stateIn将惯例 flow 转化为 StateFlow,但它需求一些装备。如果你现在不想深化细节,只想复制粘贴,那么我推荐这种组合:

val result: StateFlow<Result<UiState>> = someFlow
    .stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )

可是,如果您不确定这个看似随机的 5 秒 started 参数,请继续阅览。

stateIn 有 3 个参数(来自文档):

@param scope
开启同享的 协程效果域。
@param started 
控制同享 何时开端 和 何时中止 的战略。
@param initialValue 
state Flow 的初始值。
当运用带有 `replayExpirationMillis` 参数的 [SharingStarted.WhileSubscribed] 战略,重置 state flow 时,也会运用此值。

started能够取 3 个值:

  • Lazily:当第一个订阅者出现时开端,当 scope 被撤销时中止。
  • Eagerly:当即开端,并在 scope 被撤销时中止
  • WhileSubscribed:这就比较杂乱了

关于 一次性操作,您能够运用 LazilyEagerly。可是,如果您正在调查其他 flow,则应该运用 WhileSubscribed 来进行小而重要的优化,如下所述。

WhileSubscribed 战略

WhileSubscribed 在没有搜集者时,会撤销 上游 flow。运用stateIn创立的 StateFlow 将数据揭露给 view,但它也会调查来自 其他层app(上游) 的 flow。坚持这些 flow 处于活泼的状况,可能会导致资源糟蹋,例如,假设它们继续从数据库连接、硬件传感器等其他来源读取数据(的话,就会导致资源糟蹋)。当您的 app 进入后台时,您应该做个良好市民,中止这些协程。

WhileSubscribed 有俩参数:

public fun WhileSubscribed(
    stopTimeoutMillis: Long = 0,
    replayExpirationMillis: Long = Long.MAX_VALUE
)

Stop 超时

来自文档的描绘,如下所示:

stopTimeoutMillis 是用来装备 最后一个订阅者消失 和 上游flow中止 之间的推迟(以毫秒为单位)的。它默以为零(也就是 当即中止)。

这用途可大了去了,由于如果 view 在几分之一秒内就中止监听的话,你必定不想撤销上游 flow。这种情况总是发生——例如,当用户旋转设备时,view 会被快速连续地毁掉和从头创立。

liveData 协程 builder 中的解决计划,是 增加 5 秒的推迟,之后如果没有订阅者,那么协程将中止。WhileSubscribed(5000) 就是这么干的:

class MyViewModel(...) : ViewModel() {
    val result = userId.mapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
}

这种办法包括下面这几条内容:

  • 当用户将您的 app 发送到后台时,来自其他层的更新,将在五秒后中止,以节省电池电量。
  • 最新的值仍将被缓存,以便当用户回来它时,view 当即就能有一些数据。
  • 订阅将从头发动,新值将出现,并在可用时刷新屏幕。

Replay 的到期时刻

如果您不希望用户在脱离太久时,看到过期的数据,而且您更喜欢显现 loading 画面,请查看 WhileSubscribed 中的replayExpirationMillis 参数。在这种情况下它十分便利而且还节省了一些内存,由于缓存的值将被康复为 stateIn 中界说的初始值。回来 app 不会那么快,但却不会显现旧的数据。

replayExpirationMillis -装备 同享协程的中止 和 replay缓存的重置 之间的推迟(以毫秒为单位)(这将使 shareIn 运算符的缓存为空,并将缓存值重置为 stateIn 运算符的原始 initialValue)。它默以为 Long.MAX_VALUE(永久保存replay缓存,从不重置缓冲区)。运用零值可使缓存当即过期。

从 view 调查 StateFlow

正如咱们到目前为止所看到的,让 ViewModel 中的 StateFlow 知道他们不再监听了,对 view 来说是十分重要的。可是,就像一切与生命周期相关的工作相同,工作并没有那么简略。

为了搜集 flow,您需求一个协程。 Activities 和 fragments 供给了一堆协程 builder:

  • Activity.lifecycleScope.launch:当即发动协程,并在 activity 被毁掉时撤销它。
  • Fragment.lifecycleScope.launch:当即发动协程,并在 fragment 被毁掉时撤销它。
  • Fragment.viewLifecycleOwner.lifecycleScope.launch:当即发动协程,并在 fragment 的 view lifecycle 被毁掉时撤销协程。如果你正在修正 UI,你应该运用 view lifecycle

LaunchWhenStarted, launchWhenResumed…

名为 launchWhenX的,即launch的特殊版本,将一直等待,直到 lifecycleOwner处于 X 状况,并在lifecycleOwner低于 X 状况时,挂起协程。需求留意的是,在它们的生命周期一切者被毁掉之前,它们是不会撤销协程的

从 LiveData 迁移到 Kotlin 的 Flow

运用 launch/launchWhenX搜集 Flow,是不安全的

在 app 处于后台时接收更新,可能会导致溃散,能够经过在视图中挂起 collection,来解决这个问题。可是,当 app 处于后台时,上游 flow 仍处于活泼状况,这可能会糟蹋资源。

这意味着,到目前为止,咱们为装备 StateFlow 所做的一切都将毫无用途;可是,眼下有一个新的 API 上台了。

lifecycle.repeatOnLifecycle 来救场

这个新的协程 builder(可从 lifecycle-runtime-ktx 2.4.0-alpha01 取得)正是咱们所需求的:它在特定状况下发动协程,并在生命周期一切者低于该状况时中止协程。

从 LiveData 迁移到 Kotlin 的 Flow

不同的 Flow 搜集办法

例如,在 Fragment 中:

onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
            myViewModel.myUiState.collect { ... }
        }
    }
}

这将在 Fragment 的 view STARTED 的时分 ,开端搜集,并在回来到 STOPPED 时中止。阅览以更安全的方法从 Android UI 搜集 flow 的悉数内容。

repeatOnLifecycle API,与上述 StateFlow 攻略混合运用,能够在充分利用设备资源的一起,取得最佳功能。

从 LiveData 迁移到 Kotlin 的 Flow

StateFlow 经过 WhileSubscribed(5000) 揭露,并经过 repeatOnLifecycle(STARTED) 搜集

警告:最近增加到 Data Binding 的 StateFlow 支撑 运用 launchWhenCreated 来搜集更新,当达到稳定状况时,将开端运用 repeatOnLifecycle 来代替。

关于 Data Binding 来说,你应该随处运用 Flow,并简略地增加 asLiveData(),将其揭露给 view。当 lifecycle-runtime-ktx 2.4.0 变得稳守时,Data Binding 也会被更新。

总结

从 ViewModel 揭露数据,并从 view 中搜集数据的最佳方法是:

  • ✔️ 运用 WhileSubscribed 战略,揭露一个带有超时的 StateFlow。 [比如]
  • ✔️ 运用 repeatOnLifecycle 搜集。[比如]

任何其他组合,都将使上游 flow 坚持活泼状况,从而糟蹋资源:

  • ❌ 运用 WhileSubscribed 揭露,并在 lifecycleScope.launch/launchWhenX 中搜集
  • ❌ 运用Lazily/Eagerly揭露,并运用repeatOnLifecycle 搜集

当然,如果您不需求 Flow 的悉数功能的话……那就用 LiveData 就行了。:)

感谢 Manuel , Wojtek , Yigit , Alex Cook, Florina 还有 Chris!