理解协程、LiveData 和 Flow

从 API 1 开始,处理 Activity 的生命周期 (lifecycle) 就是个老大难的问题,基本上开发者们都看过这两张生命周期流程图:html

△ Activity 生命周期流程图

随着 Fragment 的加入,这个问题也变得更加复杂:android

△ Fragment 生命周期流程图
而开发者们面对这个挑战,给出了很是稳健的解决方案: 分层架构。

分层架构

△ 表现层 (Presentation Layer)、域层 (Domain Layer) 和数据层 (Data Layer)

如上图所示,经过将应用分为三层,如今只有最上面的 Presentation 层 (之前叫 UI 层) 才知道生命周期的细节,而应用的其余部分则能够安全地忽略掉它。git

而在 Presentation 层内部也有进一步的解决方案: 让一个对象能够在 Activity 和 Fragment 被销毁、从新建立时依然留存,这个对象就是架构组件的 ViewModel 类。下面让咱们详细看看 ViewModel 工做的细节。api

如上图,当一个视图 (View) 被建立,它有对应的 ViewModel 的引用地址 (注意 ViewModel 并无 View 的引用地址)。ViewModel 会暴露出若干个 LiveData,视图会经过数据绑定或者手动订阅的方式来观察这些 LiveData。

当设备配置改变时 (好比屏幕发生旋转),以前的 View 被销毁,新的 View 被建立:安全

这时新的 View 会从新订阅 ViewModel 里的 LiveData,而 ViewModel 对这个变化的过程彻底不知情。

归根到底,开发者在执行一个操做时,须要认真选择好这个操做的做用域 (scope)。这取决于这个操做具体是作什么,以及它的内容是否须要贯穿整个屏幕内容的生命周期。好比经过网络获取一些数据,或者是在绘图界面中计算一段曲线的控制锚点,可能所适用的做用域不一样。如何取消该操做的时间太晚,可能会浪费不少额外的资源;而若是取消的太早,又会出现频繁重启操做的状况。

在实际应用中,以咱们的 Android Dev Summit 应用为例,里面涉及到的做用域很是多。好比,咱们这里有一个活动计划页面,里面包含多个 Fragment 实例,而与之对应的 ViewModel 的做用域就是计划页面。与之相相似的,日程和信息页面相关的 Fragment 以及 ViewModel 也是同样的做用域。bash

此外咱们还有不少 Activity,而和它们相关的 ViewModel 的做用域就是这些 Activity。服务器

您也能够自定义做用域。好比针对导航组件,您能够将做用域限制在登陆流程或者结帐流程中。咱们甚至还有针对整个 Application 的做用域。markdown

有如此多的操做会同时进行,咱们须要有一个更好的方法来管理它们的取消操做。也就是 Kotlin 的协程 (Coroutine)。

协程的优点

协程的优势主要来自三个方面:网络

  1. 很容易离开主线程。咱们试过不少方法来让操做远离主线程,AsyncTask、Loaders、ExecutorServices……甚至有开发者用到了 RxJava。但协程可让开发者只须要一行代码就完成这个工做,并且没有累人的回调处理。
  2. 样板代码最少。协程彻底活用了 Kotlin 语言的能力,包括 suspend 方法。编写协程的过程就和编写普通的代码块差很少,编译器则会帮助开发者完成异步化处理。
  3. 结构并发性。这个能够理解为针对操做的垃圾搜集器,当一个操做再也不须要被执行时,协程会自动取消它。

如何启动和取消协程

在 Jetpack 组件里,咱们为各个组件提供了对应的 scope,好比 ViewModel 就有与之对应的 viewModelScope,若是您想在这个做用域里启动协程,使用以下代码便可:架构

class MainActivityViewModel : ViewModel {

    init {
        viewModelScope.launch {
            // Start

        }    
    }
}
复制代码

若是您在使用 AppCompatActivity 或 Fragment,则可使用 lifecycleScope,当 lifeCycle 被销毁时,操做也会被取消。代码以下:

class MyActivity : AppCompatActivity() {
    override fun onCreate(state: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            // Run
        }         

     }
}
复制代码

有些时候,您可能还须要在生命周期的某个状态 (启动时/恢复时等) 执行一些操做,这时您可使用 launchWhenStarted、launchWhenResumed、launchWhenCreated 这些方法:

class MyActivity : Activity {
    override fun onCreate(state: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            // Run
        }

        lifecycleScope.launchWhenResumed {
            // Run
        }
     }
}
复制代码

注意,若是您在 launchWhenStarted 中设置了一个操做,当 Activity 被中止时,这个操做也会被暂停,直到 Activity 被恢复 (Resume)。

最后一种做用域的状况是贯穿整个应用。若是这个操做很是重要,您须要确保它必定被执行,这时请考虑使用 WorkManager。好比您编写了一个发推的应用,但愿撰写的推文被发送到服务器上,那这个操做就须要使用 WorkManager 来确保执行。而若是您的操做只是清理一下本地存储,那能够考虑使用 Application Scope,由于这个操做的重要性不是很高,彻底能够等到下次应用启动时再作。

WorkManager 不是本文介绍的重点,感兴趣的朋友请参考 《WorkManager 进阶课堂 | AndroidDevSummit 中文字幕视频》

接下来咱们看看如何在 viewModelScope 里使用 LiveData。之前咱们想在协程里作一些操做,并将结果反馈到 ViewModel 须要这么操做:

class MyViewModel : ViewModel {
    private val _result = MutableLiveData<String>()
    val result: LiveData<String> = _result

    init {
        viewModelScope.launch {
            val computationResult = doComputation()
            _result.value = computationResult
          }
      }
}
复制代码

看看咱们作了什么:

  1. 准备一个 ViewModel 私有的 MutableLiveData (MLD)
  2. 暴露一个不可变的 LiveData
  3. 启动协程,而后将其操做结果赋给 MLD

这个作法并不理想。在 LifeCycle 2.2.0 以后,一样的操做能够用更精简的方法来完成,也就是 LiveData 协程构造方法 (coroutine builder):

class MyViewModel {
    val result = liveData {
        emit(doComputation())
    }
}
复制代码

这个 liveData 协程构造方法提供了一个协程代码块,这个块就是 LiveData 的做用域,当 LiveData 被观察的时候,里面的操做就会被执行,当 LiveData 再也不被使用时,里面的操做就会取消。并且该协程构造方法产生的是一个不可变的 LiveData,能够直接暴露给对应的视图使用。而 emit() 方法则用来更新 LiveData 的数据。

让咱们来看另外一个常见用例,好比当用户在 UI 中选中一些元素,而后将这些选中的内容显示出来。一个常见的作法是,把被选中的项目的 ID 保存在一个 MutableLiveData 里,而后运行 switchMap。如今在 switchMap 里,您也可使用协程构造方法:

private val itemId = MutableLiveData<String>()
val result = itemId.switchMap {
    liveData { emit(fetchItem(it)) }
}
复制代码

LiveData 协程构造方法还能够接收一个 Dispatcher 做为参数,这样您就能够将这个协程移至另外一个线程。

liveData(Dispatchers.IO) {
}
复制代码

最后,您还可使用 emitSource() 方法从另外一个 LiveData 获取更新的结果:

liveData(Dispatchers.IO) {
    emit(LOADING_STRING)
    emitSource(dataSource.fetchWeather())
}
复制代码

接下来咱们来看如何取消协程。绝大部分状况下,协程的取消操做是自动的,毕竟咱们在对应的做用域里启动一个协程时,也同时明确了它会在什么时候被取消。但咱们有必要讲一讲如何在协程内部来手动取消协程。

这里补充一个大前提: 全部 kotlin.coroutines 的 suspend 方法都是可取消的。好比这种:

suspend fun printPrimes() {
    while(true) {
        // Compute
  delay(1000)
    }
}
复制代码

在上面这个无限循环里,每个 delay 都会检查协程是否处于有效状态,一旦发现协程被取消,循环的操做也会被取消。

那问题来了,若是您在 suspend 方法里调用的是一个不可取消的方法呢?这时您须要使用 isActivate 来进行检查并手动决定是否继续执行操做:

suspend fun printPrimes() {
    while(isActive) {
        // Compute
    }
}
复制代码

LiveData 操做实践

在进入具体的操做实践环节以前,咱们须要区分一下两种操做: 单次 (One-Shot) 操做和监听 (observers) 操做。好比 Twitter 的应用:

单次操做,好比获取用户头像和推文,只须要执行一次便可。 监听操做,好比界面下方的转发数和点赞数,就会持续更新数据。

让咱们先看看单次操做时的内容架构:

如前所述,咱们使用 LiveData 链接 View 和 ViewModel,而在 ViewModel 这里咱们则使用刚刚提到的 liveData 协程构造方法来打通 LiveData 和协程,再往右就是调用 suspend 方法了。

若是咱们想监听多个值的话,该如何操做呢?

第一种选择是在 ViewModel 以外也使用 LiveData:

△ Reopsitory 监听 Data Source 暴露出来的 LiveData,同时本身也暴露出 LiveData 供 ViewModel 使用
可是这种实现方式没法体现并发性,好比每次用户登出时,就须要手动取消全部的订阅。LiveData 自己的设计并不适合这种状况,这时咱们就须要使用第二种选择: 使用 Flow。

ViewModel 模式

当 ViewModel 监听 LiveData,并且没有对数据进行任何转换操做时,能够直接将 dataSource 中的 LiveData 赋值给 ViewModel 暴露出来的 LiveData:

val currentWeather: LiveData<String> =  
    dataSource.fetchWeather()
复制代码

若是使用 Flow 的话就须要用到 liveData 协程构造方法。咱们从 Flow 中使用 collect 方法获取每个结果,而后 emit 出来给 liveData 协程构造方法使用:

val currentWeatherFlow: LiveData<String> = liveData {
    dataSource.fetchWeatherFlow().collect {
        emit(it)
    }
}
复制代码

不过 Flow 给咱们准备了更简单的写法:

val currentWeatherFlow: LiveData<String> = 
    dataSource.fetchWeatherFlow().asLiveData()
复制代码

接下来一个场景是,咱们先发送一个一次性的结果,而后再持续发送多个数值:

val currentWeather: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(dataSource.fetchWeather())
}
复制代码

在 Flow 中咱们能够沿用上面的思路,使用 emit 和 emitSource:

val currentWeatherFlow: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(
        dataSource.fetchWeatherFlow().asLiveData()
    )
}
复制代码

但一样的,这种状况 Flow 也有更直观的写法:

val currentWeatherFlow: LiveData<String> = 
    dataSource.fetchWeatherFlow()
        .onStart { emit(LOADING_STRING) }
        .asLiveData()
复制代码

接下来咱们看看须要为接收到的数据作转换时的状况。

使用 LiveData 时,若是用 map 方法作转换,操做会进入主线程,这显然不是咱们想要的结果。这时咱们可使用 switchMap,从而能够经过 liveData 协程构造方法得到一个 LiveData,并且 switchMap 的方法会在每次数据源 LiveData 更新时调用。而在方法体内部咱们可使用 heavyTransformation 函数进行数据转换,并发送其结果给 liveData 协程构造方法:

val currentWeatherLiveData: LiveData<String> =
    dataSource.fetchWeather().switchMap {
        liveData { emit(heavyTransformation(it)) }
    }
复制代码

使用 Flow 的话会简单许多,直接从 dataSource 得到数据,而后调用 map 方法 (这里用的是 Flow 的 map 方法,而不是 LiveData 的),而后转化为 LiveData 便可:

val currentWeatherFlow: LiveData<String> =
    dataSource.fetchWeatherFlow()
        .map { heavyTransformation(it) }
        .asLiveData()
复制代码

Repository 模式 Repository 通常用来进行复杂的数据转换和处理,而 LiveData 没有针对这种状况进行设计。如今经过 Flow 就能够完成各类复杂的操做:

val currentWeatherFlow: Flow<String> =
    dataSource.fetchWeatherFlow()
        .map { ... }
        .filter { ... }
        .dropWhile { ... }
        .combine { ... }
        .flowOn(Dispatchers.IO)
        .onCompletion { ... }
...
复制代码

数据源模式

而在涉及到数据源时,状况变得有些复杂,由于这时您多是在和其余代码库或者远程数据源进行交互,可是您又没法控制这些数据源。这里咱们分两种状况介绍:

1. 单次操做

若是使用 Retrofit 从远程数据源获取数值,直接将方法标记为 suspend 方法便可*:

suspend fun doOneShot(param: String) : String =
    retrofitClient.doSomething(param)
复制代码
  • Retrofit 从 2.6.0 开始支持 suspend 方法,Room 从 2.1.0 开始支持 suspend 方法。

若是您的数据源还没有支持协程,好比是一个 Java 代码库,并且使用的是回调机制。这时您可使用 suspendCancellableCoroutine 协程构造方法,这个方法是协程和回调之间的适配器,会在内部提供一个 continuation 供开发者使用:

suspend fun doOneShot(param: String) : Result<String> =
    suspendCancellableCoroutine { continuation ->
        api.addOnCompleteListener { result ->
            continuation.resume(result)
        }.addOnFailureListener { error ->
            continuation.resumeWithException(error)
        }
  }
复制代码

如上所示,在回调方法取得结果后会调用 continuation.resume(),若是报错的话调用的则是 continuation.resumeWithException()。

注意,若是这个协程已经被取消,则 resume 调用也会被忽略。开发者能够在协程被取消时主动取消 API 请求。

2. 监听操做

若是数据源会持续发送数值的话,使用 flow 协程构造方法会很好地知足需求,好比下面这个方法就会每隔 2 秒发送一个新的天气值:

override fun fetchWeatherFlow(): Flow<String> = flow {
    var counter = 0
    while(true) {
        counter++
        delay(2000)
        emit(weatherConditions[counter % weatherConditions.size])
    }
}
复制代码

若是开发者使用的是不支持 Flow 而是使用回调的代码库,则可使用 callbackFlow。好比下面这段代码,api 支持三个回调分支 onNextValue、onApiError 和 onCompleted,咱们能够获得结果的分支里使用 offer 方法将值传给 Flow,在发生错误的分支里 close 这个调用并传回一个错误缘由 (cause),而在顺利调用完成后直接 close 调用:

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) }
}
复制代码

注意在这段代码的最后,若是 API 不会再有更新,则使用 awaitClose 完全关闭这条数据通道。

相信看到这里,您对如何在实际应用中使用协程、LiveData 和 Flow 已经有了比较系统的认识。您能够重温 Android Dev Summit 上 Jose Alcérreca 和 Yigit Boyar 的演讲来巩固理解:

视频连接:v.qq.com/x/page/a302…

若是您对协程、LiveData 和 Flow 有任何疑问和想法,欢迎在评论区和咱们分享。

点击这里进一步了解 LiveData