在本系列第二篇文章 协程中的勾销和异样 | 勾销操作详解 中,咱们学到,当一个工作不再被须要时,正确地退出非常的重要。在 Android 中,您能够应用 Jetpack 提供的两个 CoroutineScopes: viewModelScope.viewModelScope:kotlinx.coroutines.CoroutineScope) 和 lifecycleScope,它们能够在 Activity、Fragment、Lifecycle 实现时退出正在运行的工作。如果您正在创立本人的 CoroutineScope,记得将它绑定到某个工作中,并在须要的时候勾销它。

然而,在有些状况下,您会心愿即便用户来到了以后界面,操作仍然可能执行实现。因而,您就不会心愿工作被勾销,例如,向数据库写入数据或者向您的服务器发送特定类型的申请。

上面咱们就来介绍实现此类情况的模式。

协程还是 WorkManager?

协程会在您的利用过程流动期间执行。如果您须要执行一个可能在利用过程之外沉闷的操作 (比方向近程服务器发送日志),在 Android 平台上倡议应用 WorkManager。WorkManager 是一个扩大库,用于那些预期会在未来的某个工夫点执行的重要操作。

请针对那些在以后过程中无效的操作应用协程,同时保障能够在用户敞开利用时勾销操作 (例如,进行一个您心愿缓存的网络申请)。那么,实现这类操作的最佳实际是什么呢?

协程的最佳实际

因为本文所介绍的模式是在协程的其它最佳实际的根底之上实现的,咱们能够借此机会回顾一下:

1. 将调度器注入到类中

不要在创立协程或调用 withContext 时硬编码调度器。

✅ 益处: 便于测试。您能够在进行单元测试或仪器测试时轻松替换掉它们。

2. 该当在 ViewModel 或 Presenter 层创立协程

如果是仅与 UI 相干的操作,则能够在 UI 层执行。如果您认为这条最佳实际在您的工程中不可行,则很有可能是您没有遵循第一条最佳实际 (测试没有注入调度器的 ViewModel 会变得更加艰难;这种状况下,暴露出挂起函数会使测试变得可行)。

✅ 益处: UI 层应该尽量简洁,并且不间接触发任何业务逻辑。作为代替,该当将响应能力转移到 ViewModel 或 Presenter 层实现。在 Android 中,测试 UI 层须要执行插桩测试,而执行插桩测试须要运行一个模拟器。

3. ViewModel 或 Presenter 以下的层级,该当裸露挂起函数与 Flow

如果您须要创立协程,请应用 coroutineScope 或 supervisorScope。而如果您想要将协程限定在其余作用域,请持续浏览,接下来本文将对此进行探讨。

✅ 益处: 调用者 (通常是 ViewModel 层) 能够管制这些层级中工作的执行和生命周期,也能够在须要时勾销这些工作。

协程中那些不该当被勾销的操作

假如咱们的利用中有一个 ViewModel 和一个 Repository,它们的相干逻辑如下:

class MyViewModel(private val repo: Repository) : ViewModel() {  fun callRepo() {    viewModelScope.launch {      repo.doWork()    }  }}class Repository(private val ioDispatcher: CoroutineDispatcher) {  suspend fun doWork() {    withContext(ioDispatcher) {      doSomeOtherWork()     veryImportantOperation() // 它不该当被勾销    }  }}

咱们不心愿用 viewModelScope 来管制 veryImportantOperation(),因为 viewModelScope 随时都可能被勾销。咱们想要此操作的运行时长超过 viewModelScope,这个目标要如何达成呢?

咱们须要在 Application 类中创立本人的作用域,并在由它启动的协程中调用这些操作。这个作用域该当被注入到那些须要它的类中。

与稍后将在本文中看到的其余解决方案 (如 GlobalScope) 相比,创立本人的 CoroutineScope 的益处是您能够依据本人的想法对其进行配置。无论您是须要 CoroutineExceptionHandler,还是想应用本人的线程池作为调度器,这些常见的配置都能够放在本人的 CoroutineScope 的 CoroutineContext 中。

您能够称其为 applicationScope。applicationScope 必须蕴含一个 SupervisorJob(),这样协程中的故障便不会在层级间流传 (见本系列第三篇文章: 协程中的勾销和异样 | 异样解决详解):

class MyApplication : Application() {  // 不须要勾销这个作用域,因为它会随着过程完结而完结   val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)}

因为咱们心愿它在利用过程存活期间始终保持活动状态,所以咱们不须要勾销 applicationScope,进而也不须要放弃 SupervisorJob 的援用。当协程所需的生存期比调用处作用域的生存期更长时,咱们能够应用 applicationScope 来运行协程。

从 application CoroutineScope 创立的协程中调用那些不该当被勾销的操作

 

每当您创立一个新的 Repository 实例时,请传入下面创立的 applicationScope。对于测试,能够参考后文的 Testing 局部。

应该应用哪种协程结构器?

您须要基于 veryImportantOperation 的行为来应用 launch 或 async 启动新的协程:

  • 如果须要返回后果,请应用 async 并调用 await 来期待其实现;
  • 如果不是,请应用 launch 并调用 join 来期待其实现。请留神,如 本系列第三局部所述,您必须在 launch 块外部手动解决异样。

上面是应用 launch 启动协程的形式:

class Repository(  private val externalScope: CoroutineScope,  private val ioDispatcher: CoroutineDispatcher) {  suspend fun doWork() {    withContext(ioDispatcher) {      doSomeOtherWork()      externalScope.launch {        //如果这里会抛出异样,那么要将其包裹进 try/catch 中;        //或者依赖 externalScope 的 CoroutineScope 中的 CoroutineExceptionHandler         veryImportantOperation()      }.join()    }  }}

或应用 async:

class Repository(  private val externalScope: CoroutineScope,  private val ioDispatcher: CoroutineDispatcher) {  suspend fun doWork(): Any { // 在后果中应用特定类型    withContext(ioDispatcher) {      doSomeOtherWork()      return externalScope.async {        // 异样会在调用 await 时裸露,它们会在调用了 doWork 的协程中流传。        // 留神,如果正在调用的上下文被勾销,那么异样将会被疏忽。        veryImportantOperation()    }.await()    }  }}

在任何状况下,都无需改变下面的 ViewModel 的代码。就算 ViewModelScope 被销毁,应用 externalScope 的工作也会继续运行。就像其余挂起函数一样,只有在 veryImportantOperation() 实现之后,doWork() 才会返回。

有没有更简略的解决方案呢?

另一种能够在一些用例中应用的计划 (可能是任何人都会首先想到的计划),便是将 veryImportantOperation 像上面这样用 withContext 封装进 externalScope 的上下文中:

class Repository(  private val externalScope: CoroutineScope,  private val ioDispatcher: CoroutineDispatcher) {  suspend fun doWork() {    withContext(ioDispatcher) {      doSomeOtherWork()      withContext(externalScope.coroutineContext) {        veryImportantOperation()      }    }  }}

然而,此办法有上面几个注意事项,应用的时候须要留神:

  • 如果调用 doWork() 的协程在 veryImportantOperation 开始执行时被退出,它将继续执行直到下一个退出节点,而不是在 veryImportantOperation 完结后退出;
  • CoroutineExceptionHandler 不会如您预期般工作,这是因为在 withContext 中应用上下文时,异样会被从新抛出。

测试

因为咱们可能须要同时注入调度器和 CoroutineScop,那么这些场景里别离须要注入什么呢?

测试时要注入什么

???? 阐明文档: 

  • TestCoroutineDispatcher
  • MainCoroutineRule
  • TestCoroutineScope
  • AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()

代替计划

其实还有一些其余的形式能够让咱们应用协程来实现这一行为。不过,这些解决方案不是在任何条件下都能有条理地实现。上面就让咱们看看一些代替计划,以及为何实用或者不实用,何时应用或者不应用它们。

❌ GlobalScope

上面是几个不应该应用 GlobalScope 的理由:

  • 诱导咱们写出硬编码值 。间接应用 GlobalScope 可能会让咱们偏向于写出硬编码的调度器,这是一种很差的实际形式。
  • 导致测试十分艰难 。因为您的代码会在一个不受管制的作用域中执行,您将无奈对从中启动的工作进行治理。
  • 就如同咱们对 applicationScope 所做的那样,您无奈为所有协程都提供一个通用的、内建于作用域中的 CoroutineContext。相同,您必须传递一个通用的 CoroutineContext 给 GlobalScope 启动的所有协程。

倡议: 不要间接应用它。

❌ Android 中的 ProcessLifecycleOwner 作用域

在 Android 中的 androidx.lifecycle:lifecycle-process 库中,有一个 applicationScope,您能够应用  ProcessLifecycleOwner.get().lifecycleScope 来调用它。

在应用它时,您须要注入一个 LifecycleOwner 来代替咱们之前注入的 CoroutineScope。在生产环境中,您须要传入 ProcessLifecycleOwner.get();而在单元测试中,您能够用 LifecycleRegistry 来创立一个虚构的 LifecycleOwner。
 
留神,这个作用域的默认 CoroutineContext 是 Dispatchers.Main.immediate,所以它可能不太适宜去执行后台任务。就像应用 GlobalScope 时那样,您也须要传递一个通用的 CoroutineContext 到所有通过 GlobalScope 启动的协程中。

因为上述起因,此代替计划相比起间接在 Application 类中创立一个 CoroutineScope 要麻烦许多。而且,我集体不喜爱在 ViewModel 或 Presenter 层之下与 Android lifecycle 建设关系,我心愿这些层级是平台无关的。

倡议: 不要间接应用它。

⚠️  特地阐明**

如果您将您的 applicationScope 中的 CoroutineContext 等于 GlobalScope 或 ProcessLifecycleOwner.get().lifecycleScope,您就能够像上面这样间接应用它:

class MyApplication : Application() {  val applicationScope = GlobalScope}

您依然能够取得上文所述的所有长处,并且未来能够依据须要轻松进行更改。

❌ ✅ 应用 NonCancellable

正如您在本系列第二篇文章 协程中的勾销和异样 | 勾销操作详解 中看到的,您能够应用 withContext(NonCancellable) 在被勾销的协程中调用挂起函数。咱们建议您应用它来进行可挂起的代码清理,然而,您不应该滥用它。

这样做的危险很高,因为您将会无法控制协程的执行。的确,它能够使代码更简洁,可读性更强,但与此同时,它也可能在未来引起一些无奈预测的问题。

应用示例如下:

class Repository(  private val ioDispatcher: CoroutineDispatcher) {  suspend fun doWork() {    withContext(ioDispatcher) {      doSomeOtherWork()    withContext(NonCancellable){        veryImportantOperation()      }    }  }}

只管这个计划很有诱惑力,然而您可能无奈总是晓得 someImportantOperation() 背地有什么逻辑。它可能是一个扩大库;也可能是一个接口背地的实现。它可能会导致各种各样的问题:

  • 您将无奈在测试中完结这些操作;
  • 应用提早的有限循环将永远无奈被勾销;
  • 从其中收集 Flow 会导致 Flow 也变得无奈从内部勾销;
  • …...

而这些问题会导致呈现轻微且十分难以调试的谬误。

倡议: 仅用它来挂起清理操作相干的代码。

每当您须要执行一些超出以后作用域范畴的工作时,咱们都建议您在您本人的 Application 类中创立一个自定义作用域,并在此作用域中执行协程。同时要留神,在执行这类工作时,防止应用 GlobalScope、ProcessLifecycleOwner 作用域或 NonCancellable。