深入理解協程、LiveData 和 Flow

2021-01-10 網易

  從 API 1 開始,處理 Activity 的生命周期 (lifecycle) 就是個老大難的問題,基本上開發者們都看過這兩張生命周期流程圖:

  

  隨著 Fragment 的加入,這個問題也變得更加複雜:

  

  而開發者們面對這個挑戰,給出了非常穩健的解決方案: 分層架構。

  分層架構

  如上圖所示,通過將應用分為三層,現在只有最上面的 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)。

  協程的優勢

  協程的優點主要來自三個方面:

  

很容易離開主線程。我們試過很多方法來讓操作遠離主線程,AsyncTask、Loaders、ExecutorServices……甚至有開發者用到了 RxJava。但協程可以讓開發者只需要一行代碼就完成這個工作,而且沒有累人的回調處理。樣板代碼最少。協程完全活用了 Kotlin 語言的能力,包括 suspend 方法。編寫協程的過程就和編寫普通的代碼塊差不多,編譯器則會幫助開發者完成異步化處理。結構並發性。這個可以理解為針對操作的垃圾收集器,當一個操作不再需要被執行時,協程會自動取消它。

如何啟動和取消協程

  在 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() val result: LiveData = _result init { viewModelScope.launch { val computationResult = doComputation() _result.value = computationResult } }}

  看看我們做了什麼:

  

準備一個 ViewModel 私有的 MutableLiveData (MLD)暴露一個不可變的 LiveData啟動協程,然後將其操作結果賦給 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()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 = dataSource.fetchWeather()

  如果使用 Flow 的話就需要用到 liveData 協程構造方法。我們從 Flow 中使用 collect 方法獲取每一個結果,然後 emit 出來給 liveData 協程構造方法使用:

  val currentWeatherFlow: LiveData = liveData { dataSource.fetchWeatherFlow().collect { emit(it) }}

  不過 Flow 給我們準備了更簡單的寫法:

  val currentWeatherFlow: LiveData = dataSource.fetchWeatherFlow().asLiveData()

  接下來一個場景是,我們先發送一個一次性的結果,然後再持續發送多個數值:

  val currentWeather: LiveData = liveData { emit(LOADING_STRING) emitSource(dataSource.fetchWeather())}

  在 Flow 中我們可以沿用上面的思路,使用 emit 和 emitSource:

  val currentWeatherFlow: LiveData = liveData { emit(LOADING_STRING) emitSource( dataSource.fetchWeatherFlow().asLiveData() )}

  但同樣的,這種情況 Flow 也有更直觀的寫法:

  val currentWeatherFlow: LiveData = dataSource.fetchWeatherFlow() .onStart { emit(LOADING_STRING) } .asLiveData()

  接下來我們看看需要為接收到的數據做轉換時的情況。

  使用 LiveData 時,如果用 map 方法做轉換,操作會進入主線程,這顯然不是我們想要的結果。這時我們可以使用 switchMap,從而可以通過 liveData 協程構造方法獲得一個 LiveData,而且 switchMap 的方法會在每次數據源 LiveData 更新時調用。而在方法體內部我們可以使用 heavyTransformation 函數進行數據轉換,並發送其結果給 liveData 協程構造方法:

  val currentWeatherLiveData: LiveData = dataSource.fetchWeather().switchMap { liveData { emit(heavyTransformation(it)) } }

  使用 Flow 的話會簡單許多,直接從 dataSource 獲得數據,然後調用 map 方法 (這裡用的是 Flow 的 map 方法,而不是 LiveData 的),然後轉化為 LiveData 即可:

  val currentWeatherFlow: LiveData = dataSource.fetchWeatherFlow() .map { heavyTransformation(it) } .asLiveData()

  Repository 模式Repository 一般用來進行複雜的數據轉換和處理,而 LiveData 沒有針對這種情況進行設計。現在通過 Flow 就可以完成各種複雜的操作:

  val currentWeatherFlow: Flow = 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 = 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 = 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 = 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 的演講來鞏固理解:

相關焦點

  • 深入理解 Kotlin 協程 Coroutine(3)
    前面有兩篇文章介紹過協程,加上這篇,基本上介紹得差不多了~深入理解 Kotlin Coroutine 深入理解 Kotlin
  • 即學即用Kotlin - 協程
    上周在內部分享會上大佬同事分享了關於 Kotlin 協程的知識,之前有看過 Kotlin 協程的一些知識,以為自己還挺了解協程的,結果...在這一次分享中,發現 Flow 和 Channel 這一塊兒知識是自己不怎麼了解的,本文也將著重和大家聊一聊這一塊兒的內容,協程部分將分為三篇,本文是第一篇:「《即學即用Kotlin -
  • Unity3D協程——線程(Thread)和協程(Coroutine)
    很多小夥伴會分不清線程和協程的區別,這也是很多初級程序面試經常遇到的題目,在此特出一個系列,專門講解Unity協程。
  • 協程中的取消和異常 | 核心概念介紹
    本次系列文章 "協程中的取消和異常" 也是 Android 協程相關的內容,我們將與大家深入探討協程中關於取消操作和異常處理的知識點和技巧。 當我們需要避免多餘的處理來減少內存浪費並節省電量時,取消操作就顯得尤為重要;而妥善的異常處理也是提高用戶體驗的關鍵。
  • 計算機語言協程的歷史、現在和未來
    計算機科學是一門應用科學,幾乎所有概念都是為了理解或解決實際問題而生。協程(Coroutine)的出現也不例外。
  • 用yield實現Python協程
    上一篇 理解python中的yield關鍵字 介紹了使用yield實現生成器函數,這一篇我們來繼續深入的了解一下yield,用yield實現協程。先來解答一下上一篇留下的問題:下面的代碼為什麼第二次調用next列印None呢?
  • 乾貨 TensorFlow之深入理解AlexNet
    前言前面看了一些Tensorflow的文檔和一些比較有意思的項目,發現這裡面水很深的,需要多花時間好好從頭了解下,尤其是cv這塊的東西,特別感興趣,接下來一段時間會開始深入了解ImageNet比賽中中獲得好成績的那些模型: AlexNet、GoogLeNet、VGG(對就是之前在nerual network用的pretrained的model
  • 在 Android 開發中使用協程 | 上手指南
    然後,在 coroutineScope 代碼塊內,launch 將會在新的 scope 中啟動協程,隨著協程的啟動完成,scope 會對其進行追蹤。最後,一旦所有在 coroutineScope 內啟動的協程都完成後,loadLots 方法就可以輕鬆地返回了。注意: scope 和協程之間的父子關係是使用 Job 對象進行創建的。但是您不需要深入去了解,只要知道這一點就可以了。
  • Unity3D協程——協程的執行原理
    接上篇文章Unity3D協程——線程(Thread)和協程(Coroutine)協程是一個分部執行,
  • Python 進程、線程和協程實戰指北
    前言前些日子寫過幾篇關於線程和進程的文章,概要介紹了Python內置的線程模塊(threading)和進程模塊(multiprocessing)的使用方法,側重點是線程間同步和進程間同步。隨後,陸續收到了不少讀者的私信,諮詢進程、線程和協程的使用方法,進程、線程和協程分別適用於何種應用場景,以及混合使用進程、線程和協程的技巧。
  • Python協程:概念及其用法
    對於協程,我表示其效率確非多線程能比,但本人對此了解並不深入,因此最近幾日參考了一些資料,學習整理了一番,在此分享出來僅供大家參考,如有謬誤請指正,多謝。申明:本文介紹的協程是入門級別,大神請繞道而行,謹防入坑。文章思路:本文將先介紹協程的概念,然後分別介紹Python2.x與3.x下協程的用法,最終將協程與多線程做比較並介紹異步爬蟲模塊。
  • 利用C語言中的setjmp和longjmp,來實現異常捕獲和協程
    我們用生產者和消費者來簡單體會一下協程和線程的區別:2. 線程中的生產者和消費者生產者和消費者是 2 個並行執行的序列,通常用 2 個線程來執行;生產者在生產商品時,消費者處於等待狀態(阻塞)。生產完成後,通過信號量通知消費者去消費商品;消費者在消費商品時,生產者處於等待狀態(阻塞)。消費結束後,通過信號量通知生產者繼續生產商品。
  • GO語言:協程——Goroutine
    Go語言的協程——Goroutine 進程(Process),線程(Thread),協程(Coroutine,也叫輕量級線程) 進程進程是一個程序在一個數據集中的一次動態執行過程,可以簡單理解為「正在執行的程序」,它是CPU資源分配和調度的獨立單位。
  • Kotlin協程優雅的與Retrofit纏綿
    涉及到Kotlin的協程、擴展方法、DSL,沒有基礎的小夥伴,先去了解這三樣東西,本篇文章不再進行講解。DSL可以看看我寫這篇簡介在網絡請求中,我們需要關注的隱式問題就是:頁面生命周期的綁定,關閉頁面後需要關閉未完成的網絡請求。為此,各位前輩,是八仙過海、各顯神通。我也是從學習、模仿前輩,到自我理解的轉變。1.
  • 在 Android 開發中使用協程 | 代碼實戰
    在閱讀本文之前,建議您先閱讀本系列的前兩篇文章,關於在 Android 開發中使用協程的背景介紹和上手指南。前兩篇文章主要是介紹了如何使用協程來簡化代碼,在 Android 上保證主線程安全,避免任務洩漏。以此為背景,我們認為使用協程是在處理後臺任務和簡化 Android 回調代碼的絕佳方案。
  • Python 對協程的支持,你得了解下
    協程由於由程序主動控制切換,沒有線程切換的開銷,所以執行效率極高。對於IO密集型任務非常適用,如果是cpu密集型,推薦多進程+協程的方式。在Python3.4之前,官方沒有對協程的支持,存在一些三方庫的實現,比如gevent和Tornado。3.4之後就內置了asyncio標準庫,官方真正實現了協程這一特性。
  • Unity協程性能分析
    今天我們將分析協程的啟動消耗和執行消耗。協程就是C#中的迭代器函數。這意味著它返回System.Collections.IEnumerator並且函數體中至少有一個yield return X語句。啟用協程來調用它的話,就會像Update函數一樣,每幀恢復一次調用。
  • Python與協程從Python2—Python3
    協程介紹協程,又稱微線程、纖程,英文名Coroutine;用一句話說明什麼是線程的話:協程是一種用戶態的輕量級線程。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。
  • 揭秘Python並發編程——協程
    Python並發編程一直是進階當中不可跨越的一道坎,其中包括進程、線程、協程,今天我們就來聊一聊協程。協程的定義很簡單,從頭到尾只有一條任務線在進行,就像是你可以在煮雞蛋的時候背單詞,但無論是煮雞蛋還是背單詞,始終都是你一個人在進行任務,線程的概念稍有不同,是把一個人分成兩個人,一個在煮雞蛋,一個在背單詞,我們直接上代碼看一下:這是一段普通的代碼,我們分別讓不同的url睡眠不同的時間,總共是10s,看一下運行結果:然後,我們使用協程來執行這段代碼:
  • Python協程與異步編程超全總結
    協程:又稱為微線程,在一個線程中執行,執行函數時可以隨時中斷,由程序(用戶)自身控制,執行效率極高,與多線程比較,沒有切換線程的開銷和多線程鎖機制。Python中異步IO操作是通過asyncio來實現的。異步IO異步IO的asyncio庫使用事件循環驅動的協程實現並發。