关于android:理解协程LiveData-和-Flow

71次阅读

共计 7496 个字符,预计需要花费 19 分钟才能阅读完成。

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

△ Activity 生命周期流程图

随着 Fragment 的退出,这个问题也变得更加简单:

△ Fragment 生命周期流程图

而开发者们面对这个挑战,给出了十分持重的解决方案: 分层架构。

分层架构

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

如上图所示,通过将利用分为三层,当初只有最下面的 Presentation 层 (以前叫 UI 层) 才晓得生命周期的细节,而利用的其余局部则能够平安地疏忽掉它。

而在 Presentation 层外部也有进一步的解决方案: 让一个对象能够在 Activity 和 Fragment 被销毁、从新创立时仍然留存,这个对象就是架构组件的 ViewModel 类。上面让咱们具体看看 ViewModel 工作的细节。

如上图,当一个视图 (View) 被创立,它有对应的 ViewModel 的援用地址 (留神 ViewModel 并没有 View 的援用地址)。ViewModel 会暴露出若干个 LiveData,视图会通过数据绑定或者手动订阅的形式来察看这些 LiveData。

当设施配置扭转时 (比方屏幕产生旋转),之前的 View 被销毁,新的 View 被创立:

这时新的 View 会从新订阅 ViewModel 里的 LiveData,而 ViewModel 对这个变动的过程齐全不知情。

归根到底,开发者在执行一个操作时,须要认真抉择好这个操作的作用域 (scope)。这取决于这个操作具体是做什么,以及它的内容是否须要贯通整个屏幕内容的生命周期。比方通过网络获取一些数据,或者是在绘图界面中计算一段曲线的管制锚点,可能所实用的作用域不同。如何勾销该操作的工夫太晚,可能会节约很多额定的资源;而如果勾销的太早,又会呈现频繁重启操作的状况。

在理论利用中,以咱们的 Android Dev Summit 利用为例,外面波及到的作用域十分多。比方,咱们这里有一个流动打算页面,外面蕴含多个 Fragment 实例,而与之对应的 ViewModel 的作用域就是打算页面。与之相相似的,日程和信息页面相干的 Fragment 以及 ViewModel 也是一样的作用域。

此外咱们还有很多 Activity,而和它们相干的 ViewModel 的作用域就是这些 Activity。

您也能够自定义作用域。比方针对导航组件,您能够将作用域限度在登录流程或者结账流程中。咱们甚至还有针对整个 Application 的作用域。

有如此多的操作会同时进行,咱们须要有一个更好的办法来治理它们的勾销操作。也就是 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)

但同样的,这种状况 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 的演讲来 视频 坚固了解。

如果您对协程、LiveData 和 Flow 有任何疑难和想法,欢送在评论区和咱们分享。

正文完
 0