/ android

【DevTips】Kotlin Coroutines Recipes

说起来,也有用 Kotlin Coroutines 一段时间了,多多少少会有些经验和想法。刚好前些天参加了一个 Kotlin Everywhere 的开发者会议,所以也打算就 Kotlin Coroutines 聊聊一些经验。

文章标题名为 Kotlin Coroutines Recipes,Recipes 是菜谱的意思,这里不会去翻译官网上的文档,翻译这种事情很多人做过了,这里我主要结合一些例子去提供一些实践的经验。当然了,这里写的不一定就是对的,所以请保留态度地阅读,非常欢迎讨论。同时,没有特别说明,下面的例子会使用 Kotlin 标准库里的方法和类,不使用 AndroidX 提供的扩展方法和属性,除非有特别说明。

Coroutine Scope

Kotlin Coroutines 是结构化的并行(structure concurency),管理 Coroutine 从 Scope 开始。

首先,建议把 Scope 在一个拥有明确声明周期的组件实例化。GlobalScope 是非常不建议用的,除非你要在 Application 生命周期执行一些异步任务,同时是 fire and forget 的,比如初始化某些库。

何为有明确的生命周期?Activity,Fragment,ViewModel (AAC 里的),甚至是 View (考虑到 onAttachToWindow/onDetachFromWindow) 都可以。

你可以通过实现这个接口,并把具体实现委托给一个实例,就像这样子:

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
   }
}

接下来,你就可以调用 launch() 启动一个在主线程执行的 Coroutines,或者在里面 Plus 上需要的 CoroutineContext ,并可以在适当的时候调用 cancel() 来取消自身以及启动的所有 Coroutines。注意这里的 自身,意思是取消了就无法再调用 launch() 来启动一个 Corountine 了。因此,对于 Activtiy 来说,最佳的 cancel() 时机是在 onDestroy() ,对 ViewModel 来说,在 onClear() 里。

如果想启动一个 Coroutine,但是能随时中止而且不会导致其他 Coroutine 取消,那么需要对 launch() 方法返回的 Job 引用到一个地方,然后在需要的时候取消就行了。

之所以这样子,是因为 MainScope() 的 CoroutineContext 里面包含 SupervisorJob,当子 Job 取消的时候,SupervisorJob 以及 parent job 不会受影响。

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

举个例子,对于一个自定义 View,假如希望在被 attach 到 Window,也就是拥有一个能绘制的平面后,每隔 1s 重绘一次,那么可以这样子:

class CustomView(
   context: Context?,
   attrs: AttributeSet?
) : View(context, attrs), CoroutineScope by MainScope() {
   private var drawingJob: Job? = null

   override fun onAttachedToWindow() {
       super.onAttachedToWindow()
       delayToDraw()
   }

   override fun onDetachedFromWindow() {
       drawingJob?.cancel()
       super.onDetachedFromWindow()
   }

   override fun onDraw(canvas: Canvas) {
       super.onDraw(canvas)
       canvas.drawColor(Color.RED)
   }

   private fun delayToDraw() {
       drawingJob = launch {
           while (true) {
               delay(1_000)
               invalidate()
               Log.i("vvv", "on invalidate")
           }
       }
   }
}

在使用 RxJava 进行异步网络请求的时候,通常会使用一个库叫 RxLifecycle,对 upstream 你通常会这么做:

.compose(RxLifecycle.bindUntilEvent(lifecycle, ActivityEvent.DESTROY))

那么,在 Kotlin Coroutines 的世界,你可以使用 Scope 一次过取消你的网络请求:

data class User(val name: String)

interface ApiService {
   suspend fun getUser(): User
}

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
   @Inject
   private lateinit var apiService: ApiService

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       btn.setOnClickListener {
           onClick()
       }
   }

   private fun onClick() = launch(CoroutineExceptionHandler { _, throwable ->
       throwable.printStackTrace()
   }) {
       val user = withContext(Dispatchers.IO) {
           apiService.getUser()
       }
       btn.text = user.name
   }

   override fun onDestroy() {
       cancel()
       super.onDestroy()
   }
}

当然,作为一个 UI controller,Activity 和 Fragment 不应该去做异步的事情,除非你希望使用 Kotlin Coroutines 做一些非阻塞挂起的操作(比如使用 delay 方法做倒计时)。下一节将介绍在 AAC 里,如何管理和组织 Kotlin Coroutines 以及 suspend 方法。

总的来说,以上内容说了:

  • 使用 Scope 来管理你的 Coroutines
  • 在有明确生命周期的组件实例化你的 Scope
  • 在销毁的时候,也把 Scope 销毁

当然,AndroidX 里有一个叫 LifecycleScope 的东西,帮你在 LiveData 断开观察的时候去取消 Coroutines,具体可以参考这里.