iT邦幫忙

2021 iThome 鐵人賽

DAY 27
0
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 27

day27 coroutine和任務的愛情長跑,application和workManager

之後四天,會講講以前面知識做基礎開發時,會遇到的問題

在前面,講到了coroutine是什麼,不妥善處理會有workleak,也介紹了他的exceptiom和取消,以及官方提供的lifecycleScope和viewModelScope

但是昨天coroutine托夢告訴我,他也想要細水長流的愛情呀,那些launch和async的任務都只是過客而已,利用完他就走了,心很累,我覺得吧,我們跟coroutine都甚麼交情了,這事肯定給她安排

那要application內還是application外,首先作為開發者,你要評估你任務的執行時間,如果你要執行的任務比application就應該選workManager

application內的長期任務

在這之前,我講了許多coroutine的實用工具,也示範了一次性網路請情、socket連線、資料流的概述、paging3應用等等,但這些都只是為了在某個ui或某些情境的短期任務,當然並不是他不好,我也是參考了coroutine的最佳做法介紹的,只是長期任務,我們沒講到呀

那要如何為應用內的長期任務使用coroutine,其實答案已經講出來了,就是在Application class內創建coroutine

class MyApp:Application() {

    val applicationScope = CoroutineScope(SupervisorJob())
    ...
}

注意我們並不需要對此作出取消,因為現在的應用場景是這個corouitne應該要和application存活一樣久 而現在,我們可以利用這個作用域,運行比調用作用域生命週期更長的任務

實際範例

我直接跟官方blog借範例

//注入applicationScope
class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      externalScope.launch {
        // if this can throw an exception, wrap inside try/catch
        // or rely on a CoroutineExceptionHandler installed
        // in the externalScope's CoroutineScope
        veryImportantOperation()
      }.join()
    }
  }
}
//注入applicationScope
class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork(): Any { // Use a specific type in Result
    withContext(ioDispatcher) {
      doSomeOtherWork()
      return externalScope.async {
        // Exceptions are exposed when calling await, they will be
        // propagated in the coroutine that called doWork. Watch
        // out! They will be ignored if the calling context cancels.
        veryImportantOperation()
      }.await()
    }
  }
}

上面的範例,即使viewModelScope已經destory了,在applicationScope的任務也會持續進行,並且doWork只會在veryImportantOperation完成後才回傳

為什麼不用withCotext

這個方法是這樣被提出的,很多人覺得用withContext把任務丟去externalScope就可以了,但人生就沒這麼簡單

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

這個code會遇到兩個問題

  1. 如果運行dowork的coroutine在執行veryImportantOperation時被取消,那他將會持續執行,而到下一個取消
  2. ExceptionHandler會不如預期的運行,而Exception會被重新丟出
//withContext source code
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
    // compute new context
    val oldContext = uCont.context
    val newContext = oldContext + context
    // always check for cancellation of new context
    newContext.ensureActive()

在withContext裡面,會將oldContext和newContext合併,以dispatcher而言,就是切換的動作,但如果像上面的範例呢,這個plus會把job一起換掉,然後就有問題了

為什麼不用GlobalScope呢

  1. 可配置性,自己創建的coroutine可以配置Dispatcher、Exception Handler、SupervisorJob等等前面提過的操作
  2. hardcode
  3. 測試會變麻煩

⚠️ Disclaimer
If it turns out that the CoroutineContext of your applicationScope matches the GlobalScope or ProcessLifecycleOwner.get().lifecycleScope one, you can directly assign them as follows

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

博文給了免責申明,我也貼一下,如果你判斷你的使用需求符合GlobalScope或ProcessLifecycleOwner,你也可以用

更多的細節請看博文

為什麼不用NonCancellable

  1. 你無法在測試中停止veryImportantOperation的執行
  2. 無限迴圈的delay會無法幫助取消coroutine
  3. flow將無法從外部取消

我這篇有講到NonCancellable

application外的長期任務 - WorkManager

另一個解法,是為了給在application外執行的任務,對workManager不熟悉的,請先看這篇,那要在workManager裡使用coroutine,其實也非常簡單,coroutine作為官方首選庫,直接被包在一起,這邊找最新版本

 val work_version = "2.6.0"
// Kotlin + coroutines
implementation("androidx.work:work-runtime-ktx:$work_version")

那他被包的有多簡單,首先繼承自CoroutineWorker而不是Worker,可以看到doWork已經被包成suspend function了,而suspend的寫法,也跟之前的一樣

class CoroutineDownloadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        withContext(Dispatchers.IO) {
            val data = downloadSynchronously("https://www.google.com")
            saveData(data)
            return Result.success()
        }
    }
}

對的,非常簡單,其他的做法可以參考這篇workManager的github code sample

那coroutine的job要怎麼處理???
在workManager裡面有個cancel(),可以取消或停止任務,而最棒的是,他會主動在coroutine丟出CancellationException去取消coroutine

coroutine:所以愛會消失對不對?我最後還是被取消了QQ

連結

必看

博文 coroutines-patterns-for-work-that-shouldnt-be-cancelled
WorkManager


上一篇
day26 老闆我趕時間,給我最快完成的料理 select
下一篇
day28 等一下啦,會壞掉的/// Coroutine併發操作的重複請求
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30

1 則留言

0
juck30808
iT邦新手 3 級 ‧ 2021-10-12 18:36:41

恭喜大大即將完賽XD !!!

我要留言

立即登入留言