原文链接
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(可变)数据持有者,揭露 一次性操作 的成果
这是经典形式,在这种形式中,你能用 协程的成果 来改动 状况持有者:
运用 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:
运用 可变数据容器(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)
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
由于状况持有者总是有一个值,所以最好将 UI状况 封装在某种支撑 Loading
、Success
和 Error
等状况的Result类中。
由于必须进行一些 装备,因而等效的 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)
运用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 履行此操作,看起来十分类似:
带参的 一次性数据 加载 (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
一切更新:
调查带有参数的 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 转化:
调查带有参数的 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
:这就比较杂乱了
关于 一次性操作,您能够运用 Lazily
或 Eagerly
。可是,如果您正在调查其他 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 状况时,挂起协程。需求留意的是,在它们的生命周期一切者被毁掉之前,它们是不会撤销协程的。
运用 launch/launchWhenX
搜集 Flow,是不安全的
在 app 处于后台时接收更新,可能会导致溃散,能够经过在视图中挂起 collection,来解决这个问题。可是,当 app 处于后台时,上游 flow 仍处于活泼状况,这可能会糟蹋资源。
这意味着,到目前为止,咱们为装备 StateFlow 所做的一切都将毫无用途;可是,眼下有一个新的 API 上台了。
lifecycle.repeatOnLifecycle 来救场
这个新的协程 builder(可从 lifecycle-runtime-ktx 2.4.0-alpha01 取得)正是咱们所需求的:它在特定状况下发动协程,并在生命周期一切者低于该状况时中止协程。
不同的 Flow 搜集办法
例如,在 Fragment 中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
这将在 Fragment 的 view STARTED
的时分 ,开端搜集,并在回来到 STOPPED
时中止。阅览以更安全的方法从 Android UI 搜集 flow 的悉数内容。
将 repeatOnLifecycle
API,与上述 StateFlow 攻略混合运用,能够在充分利用设备资源的一起,取得最佳功能。
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!