这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。
Part I: Reactive UIs
从Android的早期开始,我们就很快了解到Android的生命周期很难理解,充满了边缘案例,而保持理智的最好方法就是尽可能地避免它们。
为此,我们建议采用分层架构,这样我们就可以编写独立于UI的代码,而不用过多考虑生命周期。例如,我们可以添加一个持有业务逻辑的领域层(你的应用程序实际做什么)和一个数据层。
此外,我们了解到表现层可以被分成不同的组件,承担不同的责任。
- View--处理生命周期的回调、用户事件和Activity或Fragment的导航
- Presenter、ViewModel--为View提供数据,并且大多不知道在View中进行的生命周期。这意味着没有中断,也不需要在重新创建视图时进行清理。
撇开命名不谈,有两种机制可以将数据从ViewModel/Presenter发送到View。
- 拥有对视图的引用并直接调用它。通常与Presenters的工作方式有关。
- 将可观察的数据暴露给观察者。通常与ViewModels的工作方式有关。
这是一个在Android社区相当成熟的惯例,但你会发现有一些文章有不同意见。有数百篇博客文章以不同的方式定义Presenter、ViewModel、MVP和MVVM。我的建议是,你专注于你的表现层的特性,使用Android架构组件ViewModel。
- 在配置变化中保存下来,如旋转、地域变化、窗口大小调整、黑暗模式切换等。
- 有一个非常简单的生命周期。它有一个单一的生命周期回调,onCleared,一旦它的生命周期所有者完成,就会被调用。
ViewModel被设计为使用观察者模式来使用。
- 它不应该有对视图的引用。
- 它将数据暴露给观察者,但不知道这些观察者是什么。你可以使用LiveData来实现这一点。
当一个视图(一个Activity、Fragment或任何生命周期的所有者)被创建时,ViewModel被获得,它开始通过一个或多个LiveDatas暴露数据,而视图订阅了这些数据。
这个订阅可以用LiveData.observe设置,也可以用Data Binding库自动设置。
现在,如果设备被旋转,那么视图将被销毁(#1),并创建一个新的实例(#2)。
如果我们在ViewModel中有一个对Activity的引用,我们将需要确保。
- 当视图被销毁时清除它
- 如果视图处于transitional状态,避免访问。
但有了ViewModel LiveData,我们就不必再处理这个问题了。这就是为什么我们在《应用程序架构指南》中推荐这种方法。
Scopes
由于Activities和Fragments比ViewModels有相等或更短的寿命,我们可以开始讨论操作的范围了。
操作是你在应用中需要做的任何事情,比如从网络上获取数据、过滤结果或计算一些文本的排列。
对于你创建的任何操作,你需要考虑其范围:从启动到取消的时间范围。让我们看两个例子。
- 你在一个Activity的onStart中启动一个操作,你在onStop中停止它。
- 你在ViewModel的initblock中启动一个操作,然后在onCleared()中停止它。
看一下这个图,我们可以找到每个操作的意义所在。
- 在一个作用于Activity的操作中获取数据操作,将迫使我们在旋转后再次获取它,所以它应该被作用于ViewModel。
- 而排列文本在作用于ViewModel的操作中是没有意义的,因为在旋转之后,你的文本容器可能已经改变了形状。
显然,现实世界中的应用可以有比这些更多的作用域。例如,在Android Dev Summit应用程序中,我们可以使用。
- Fragment scopes,每个屏幕有多个
- Fragment ViewModel作用域,每屏一个
- Main Activity scopes
- Main Activity ViewModel scope
- Application scope
这可能会产生很多不同的作用域,所以管理所有的作用域会让人不知所措。我们需要一种方法来结构化这种并发性!
一个非常方便的解决方案是Kotlin Coroutines。
我们喜欢在Android中使用Coroutines有很多原因。其中一些是。
- 很容易脱离主线程。Android应用为了获得流畅的用户体验而不断地在线程间切换,而Coroutines让这一切变得超级简单。
- 有最小的代码模板。Coroutines被嵌入到语言中,所以使用诸如suspend功能的东西是很容易的。
- 结构化的并发性。这意味着你不得不定义你的操作范围,而且你可以享受一些代码层面的保证,从而消除大量的模板代码,如清理代码等。你可以把结构化并发想象成“自动取消”。
如果你想了解coroutines的介绍,可以看看Android的介绍和Kotlin的官方文档。
Part II: Launching coroutines with Architecture Components
Jetpack的架构组件提供了一堆语法糖,所以你不必担心Jobs和它们的取消行为。你只需要选择你的操作范围。
ViewModel scope
这是启动coroutine最常见的方式之一,因为大多数数据操作都是从ViewModel开始的。使用viewModelScope扩展,当ViewModel被清除时,Job会自动取消。使用viewModelScope. launch来启动coroutine。
代码语言:javascript复制class MainActivityViewModel : ViewModel {
init {
viewModelScope.launch {
// Do things!
}
}
}
Activity and Fragment scopes
同样,如果你使用lifecycleScope.launch,你可以将操作的范围限定在一个视图的特定实例上。
如果你用launchWhenResumed、launchWhenStarted或launchWhenCreated,则会将操作限制在某一生命周期状态,你甚至可以有一个更窄的范围。
代码语言:javascript复制class MyActivity : Activity {
override fun onCreate(state: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Run
}
lifecycleScope.launchWhenResumed {
// Run
}
}
}
Application scope
全应用程序范围有很好的用例:
https://medium.com/androiddevelopers/coroutines-patterns-for-work-that-shouldnt-be-cancelled-e26c40f142ad
但是首先,如果你的代码最终必须被执行,你应该考虑使用WorkManager。
ViewModel LiveData
到目前为止,我们已经看到了如何启动一个coroutine,但没有看到如何从它那里接收一个结果。你可以像这样使用一个MutableLiveData。
代码语言:javascript复制// Don't do this. Use liveData instead.
class MyViewModel : ViewModel() {
private val _result = MutableLiveData<String>()
val result: LiveData<String> = _result
init {
viewModelScope.launch {
val computationResult = doComputation()
_result.value = computationResult
}
}
}
但是,由于你将把这个结果暴露给你的视图,你可以通过使用liveData coroutine builder来节省一些模板代码,它可以启动一个coroutine,让你通过一个不可变的LiveData来暴露结果。你可以使用emit()来向它发送更新。
代码语言:javascript复制class MyViewModel : ViewModel() {
val result = liveData {
emit(doComputation())
}
}
LiveData Coroutine builder with a switchMap
在某些情况下,只要LiveData的值发生变化,你就想启动一个coroutine。例如,当你在开始数据加载操作之前,你需要一个ID参数。有一个方便的模式,那就是使用Transformations.switchMap。
代码语言:javascript复制private val itemId = MutableLiveData<String>()
val result = itemId.switchMap {
liveData { emit(fetchItem(it)) }
}
result是一个不可变的LiveData,只要itemId有新的值,就会用调用fetchItem suspend函数的结果来更新数据。
Emit all items from another LiveData
这个功能不太常见,但也可以节省一些模板代码:你可以使用emitSource传递一个LiveData数据源。当你想先发射一个初始值,然后再发射一连串的值时,这很有用。
代码语言:javascript复制liveData(Dispatchers.IO) {
emit(LOADING_STRING)
emitSource(dataSource.fetchWeather())
}
Cancelling coroutines
如果你使用上面的任何一种模式,你就不必明确地取消Job。然而,有一件重要的事情要记住:coroutine的取消是协作式的。
这意味着,如果调用的coroutine被取消了,你必须帮助Kotlin停止一个Job。比方说,你有一个启动无限循环的suspend函数。Kotlin没有办法为你停止这个循环,所以你需要合作,定期检查这个Job是否在活动状态。你可以通过检查isActive属性来做到这一点。
代码语言:javascript复制suspend fun printPrimes() {
while(isActive) {
// Compute
}
}
顺便说一下,如果你使用kotlinx.coroutines中的任何函数(如delay),你应该知道它们都是可取消的,这意味着它们会为你做这种检查。
代码语言:javascript复制suspend fun printPrimes() {
while(true) { // Ok-ish because we call delay inside
// Compute
delay(1000)
}
}
也就是说,我建议你无论如何都要添加这个检查,因为将来可能会有人删除这个延迟调用,在你的代码中引入一个微妙的错误。
One-shot vs multiple values
为了理解coroutines(以及反应式UI),我们需要对以下内容进行重要区分。
- One-shot操作。它们只运行一次,可以返回一个结果
- 返回多个值的操作。对一个数据源的订阅,可以在一段时间内发出多个值
One-shot operations with coroutines
使用suspend函数并使用viewModelScope或liveData{}调用它们是运行非阻塞操作的一种非常方便的方法。
代码语言:javascript复制class MyViewModel {
val result = liveData {
emit(repository.fetchData())
}
}
然而,当我们在监听变化时,事情就变得有点复杂了。
Receiving multiple values with LiveData
我在《LiveData beyond the ViewModel》(2018)中谈到了这个话题,在那里我谈到了,LiveData从未被设计成一个功能齐全的流构建器这一事实。
https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
现在,更好的方法是使用Kotlin的Flow(警告:有些部分仍在试验中)。Flow类似于RxJava中的反应式流功能。
然而,虽然轮子让非阻塞的一次性操作变得更容易,但这对Flow来说并不是同样的情况。Flow仍然是难以掌握的。不过,如果你想创建快速而可靠的反应式UI,我认为值得花时间来学习。由于它是语言的一部分,而且是一个小的依赖项,许多库都开始添加Flow支持(比如Room)。
因此,我们可以从数据源和存储库中暴露Flow,而不是LiveData,但ViewModel仍然暴露LiveData,因为它是生命周期感知的。
Part III: LiveData and coroutines patterns
ViewModel patterns
让我们看看一些可用于ViewModels的模式,比较一下LiveData和Flow的使用。
LiveData: Emit N values as LiveData
代码语言:javascript复制val currentWeather: LiveData<String> = dataSource.fetchWeather()
如果我们不做任何转换,我们可以简单地将一个分配给另一个。
Flow: Emit N values as LiveData
我们可以使用liveData coroutine builder和Flow上的collect(这是一个接收每个发射值的终端操作符)的组合。
代码语言:javascript复制// Don't use this
val currentWeatherFlow: LiveData<String> = liveData {
dataSource.fetchWeatherFlow().collect {
emit(it)
}
}
但由于它有很多模板代码,所以我们添加了Flow.asLiveData()扩展函数,它可以在一行中做同样的事情。
代码语言:javascript复制val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData()
LiveData: Emit 1 initial value N values from data source
如果数据源暴露了一个LiveData,我们可以使用emitSource在用emit发射一个初始值后进行批量更新。
代码语言:javascript复制val currentWeather: LiveData<String> = liveData {
emit(LOADING_STRING)
emitSource(dataSource.fetchWeather())
}
Flow: Emit 1 initial value N values from data source
同样,我们可以天真地做到这一点。
代码语言:javascript复制// Don't use this
val currentWeatherFlow: LiveData<String> = liveData {
emit(LOADING_STRING)
emitSource(
dataSource.fetchWeatherFlow().asLiveData()
)
}
但如果我们利用Flow自己的API,事情看起来就会整洁很多。
代码语言:javascript复制val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow()
.onStart { emit(LOADING_STRING) }
.asLiveData()
onStart设置初始值,这样做我们只需要向LiveData转换一次。
LiveData: Suspend transformation
比方说,你想对来自数据源的东西进行转换,但它可能是CPU密集型的,所以它是在一个suspend函数中。
你可以在数据源的LiveData上使用switchMap,然后用LiveData生成器创建coroutine。现在你只需对收到的每个结果调用emit即可。
代码语言:javascript复制val currentWeatherLiveData: LiveData<String> =
dataSource.fetchWeather().switchMap {
liveData { emit(heavyTransformation(it)) }
}
Flow: Suspend transformation
这就是Flow与LiveData相比真正的优势所在。我们可以再次使用Flow的API来更优雅地做事情。在这种情况下,我们使用Flow.map来在每次更新时应用转换。这一次,由于我们已经在一个coroutine上下文中,我们可以直接调用它。
代码语言:javascript复制val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow()
.map { heavyTransformation(it) }
.asLiveData()
Repository patterns
关于资源库没有什么好说的,因为如果你在使用Flow,你只需要使用Flow的API来转换和组合数据。
代码语言:javascript复制val currentWeatherFlow: Flow<String> =
dataSource.fetchWeatherFlow()
.map { ... }
.filter { ... }
.dropWhile { ... }
.combine { ... }
.flowOn(Dispatchers.IO)
.onCompletion { ... }
Data source patterns
再次,让我们区分一下One-shot场景和Flow。
One-shot operations in the data source
如果你正在使用一个支持suspend函数的库,如Room或Retrofit,你可以简单地从你的suspend函数中使用它们。
代码语言:javascript复制suspend fun doOneShot(param: String) : String = retrofitClient.doSomething(param)
然而,有些工具和库还不支持coroutine,而是基于回调。
在这种情况下,你可以使用suspendCoroutine或suspendCancellableCoroutine。
(我不知道你为什么要使用不可取消的版本,但请在评论中告诉我!)
代码语言:javascript复制suspend fun doOneShot(param: String) : Result<String> =
suspendCancellableCoroutine { continuation ->
api.addOnCompleteListener { result ->
continuation.resume(result)
}.addOnFailureListener { error ->
continuation.resumeWithException(error)
}.fetchSomething(param)
}
当你调用它时,你会得到一个continuation。在这个例子中,我们使用的API让我们设置了一个完成的监听器和一个失败的监听器,所以在它们的回调中,当我们收到数据或错误时,我们会调用continuation.resume或continuation.resumeWithException。
值得注意的是,如果这个coroutine被取消,resume将被忽略,所以如果你的请求需要很长的时间,这个coroutine将处于活动状态,直到其中一个回调被执行。
Exposing Flow in the data source
Flow builder
如果你需要创建一个假的数据源的实现,或者你只是需要一些简单的东西,你可以使用flow构造器,做一些类似的事情。
代码语言:javascript复制override fun fetchWeatherFlow(): Flow<String> = flow {
var counter = 0
while(true) {
counter
delay(2000)
emit(weatherConditions[counter % weatherConditions.size])
}
}
这段代码每隔两秒就会发出一个天气状况。
Callback-based APIs
如果你想把基于回调的API转换为Flow,你可以使用callbackFlow。
代码语言:javascript复制fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow {
val callback = object : Callback {
override fun onNextValue(value: T) {
offer(value)
}
override fun onApiError(cause: Throwable) {
close(cause)
}
override fun onCompleted() = close()
}
api.register(callback)
awaitClose { api.unregister(callback) }
}
它看起来令人生畏,但如果你把它拆开,你会发现它有很大的意义。
- 当我们有一个新的Value时,我们调用offer方法
- 当我们想停止发送更新时,我们调用close(cause?)
- 我们使用awaitClose来定义流程关闭时需要执行的内容,这对于取消注册回调来说是非常完美的。
总之,coroutines和Flow将继续存在。但它们并不能在所有地方取代LiveData。即使是非常有前途的StateFlow(目前是实验性的),我们仍然有Java编程语言和DataBinding的用户需要支持,所以它在一段时间内不会被废弃 :)
原文链接:https://medium.com/androiddevelopers/livedata-with-coroutines-and-flow-part-i-reactive-uis-b20f676d25d7
本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。