理解協(xié)程、LiveData 和 Flow

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


△ Activity 生命周期流程圖

隨著 Fragment 的加入,這個問題也變得更加復(fù)雜:
△ Fragment 生命周期流程圖

而開發(fā)者們面對這個挑戰(zhàn),給出了非常穩(wěn)健的解決方案: 分層架構(gòu)。

分層架構(gòu)

△ 表現(xiàn)層 (Presentation Layer)、域?qū)?(Domain Layer) 和數(shù)據(jù)層 (Data Layer)

如上圖所示,通過將應(yīng)用分為三層,現(xiàn)在只有最上面的 Presentation 層 (以前叫 UI 層) 才知道生命周期的細(xì)節(jié),而應(yīng)用的其他部分則可以安全地忽略掉它。

而在 Presentation 層內(nèi)部也有進(jìn)一步的解決方案: 讓一個對象可以在 Activity 和 Fragment 被銷毀、重新創(chuàng)建時依然留存,這個對象就是架構(gòu)組件的 ViewModel 類。下面讓我們詳細(xì)看看 ViewModel 工作的細(xì)節(jié)。

如上圖,當(dāng)一個視圖 (View) 被創(chuàng)建,它有對應(yīng)的 ViewModel 的引用地址 (注意 ViewModel 并沒有 View 的引用地址)。ViewModel 會暴露出若干個 LiveData,視圖會通過數(shù)據(jù)綁定或者手動訂閱的方式來觀察這些 LiveData。

當(dāng)設(shè)備配置改變時 (比如屏幕發(fā)生旋轉(zhuǎn)),之前的 View 被銷毀,新的 View 被創(chuàng)建:

這時新的 View 會重新訂閱 ViewModel 里的 LiveData,而 ViewModel 對這個變化的過程完全不知情。

歸根到底,開發(fā)者在執(zhí)行一個操作時,需要認(rèn)真選擇好這個操作的作用域 (scope)。這取決于這個操作具體是做什么,以及它的內(nèi)容是否需要貫穿整個屏幕內(nèi)容的生命周期。比如通過網(wǎng)絡(luò)獲取一些數(shù)據(jù),或者是在繪圖界面中計算一段曲線的控制錨點,可能所適用的作用域不同。如何取消該操作的時間太晚,可能會浪費很多額外的資源;而如果取消的太早,又會出現(xiàn)頻繁重啟操作的情況。

在實際應(yīng)用中,以我們的 Android Dev Summit 應(yīng)用為例,里面涉及到的作用域非常多。比如,我們這里有一個活動計劃頁面,里面包含多個 Fragment 實例,而與之對應(yīng)的 ViewModel 的作用域就是計劃頁面。與之相類似的,日程和信息頁面相關(guān)的 Fragment 以及 ViewModel 也是一樣的作用域。

此外我們還有很多 Activity,而和它們相關(guān)的 ViewModel 的作用域就是這些 Activity。

您也可以自定義作用域。比如針對導(dǎo)航組件,您可以將作用域限制在登錄流程或者結(jié)賬流程中。我們甚至還有針對整個 Application 的作用域。

有如此多的操作會同時進(jìn)行,我們需要有一個更好的方法來管理它們的取消操作。也就是 Kotlin 的協(xié)程 (Coroutine)。

協(xié)程的優(yōu)勢

協(xié)程的優(yōu)點主要來自三個方面:

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

如何啟動和取消協(xié)程

在 Jetpack 組件里,我們?yōu)楦鱾€組件提供了對應(yīng)的 scope,比如 ViewModel 就有與之對應(yīng)的 viewModelScope,如果您想在這個作用域里啟動協(xié)程,使用如下代碼即可:


class MainActivityViewModel : ViewModel {

    init {
        viewModelScope.launch {
            // Start

        }    
    }
}

如果您在使用 AppCompatActivity 或 Fragment,則可以使用 lifecycleScope,當(dāng) lifeCycle 被銷毀時,操作也會被取消。代碼如下:

class MyActivity : AppCompatActivity() {
    override fun onCreate(state: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            // Run
        }         

     }
}

有些時候,您可能還需要在生命周期的某個狀態(tài) (啟動時/恢復(fù)時等) 執(zhí)行一些操作,這時您可以使用 launchWhenStarted、launchWhenResumed、launchWhenCreated 這些方法:


class MyActivity : Activity {
    override fun onCreate(state: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            // Run
        }

        lifecycleScope.launchWhenResumed {
            // Run
        }
     }
}

注意,如果您在 launchWhenStarted 中設(shè)置了一個操作,當(dāng) Activity 被停止時,這個操作也會被暫停,直到 Activity 被恢復(fù) (Resume)。

最后一種作用域的情況是貫穿整個應(yīng)用。如果這個操作非常重要,您需要確保它一定被執(zhí)行,這時請考慮使用 WorkManager。比如您編寫了一個發(fā)推的應(yīng)用,希望撰寫的推文被發(fā)送到服務(wù)器上,那這個操作就需要使用 WorkManager 來確保執(zhí)行。而如果您的操作只是清理一下本地存儲,那可以考慮使用 Application Scope,因為這個操作的重要性不是很高,完全可以等到下次應(yīng)用啟動時再做。

WorkManager 不是本文介紹的重點,感興趣的朋友請參考《WorkManager 進(jìn)階課堂 | AndroidDevSummit 中文字幕視頻》。

接下來我們看看如何在 viewModelScope 里使用 LiveData。以前我們想在協(xié)程里做一些操作,并將結(jié)果反饋到 ViewModel 需要這么操作:

class MyViewModel : ViewModel {
    private val _result = MutableLiveData<String>()
    val result: LiveData<String> = _result

    init {
        viewModelScope.launch {
            val computationResult = doComputation()
            _result.value = computationResult
          }
      }
}

看看我們做了什么:

  1. 準(zhǔn)備一個 ViewModel 私有的 MutableLiveData (MLD)
  2. 暴露一個不可變的 LiveData
  3. 啟動協(xié)程,然后將其操作結(jié)果賦給 MLD

這個做法并不理想。在 LifeCycle 2.2.0 之后,同樣的操作可以用更精簡的方法來完成,也就是 LiveData 協(xié)程構(gòu)造方法 (coroutine builder):


class MyViewModel {
    val result = liveData {
        emit(doComputation())
    }
}

這個 liveData 協(xié)程構(gòu)造方法提供了一個協(xié)程代碼塊,這個塊就是 LiveData 的作用域,當(dāng) LiveData 被觀察的時候,里面的操作就會被執(zhí)行,當(dāng) LiveData 不再被使用時,里面的操作就會取消。而且該協(xié)程構(gòu)造方法產(chǎn)生的是一個不可變的 LiveData,可以直接暴露給對應(yīng)的視圖使用。而 emit() 方法則用來更新 LiveData 的數(shù)據(jù)。

讓我們來看另一個常見用例,比如當(dāng)用戶在 UI 中選中一些元素,然后將這些選中的內(nèi)容顯示出來。一個常見的做法是,把被選中的項目的 ID 保存在一個 MutableLiveData 里,然后運行 switchMap?,F(xiàn)在在 switchMap 里,您也可以使用協(xié)程構(gòu)造方法:


private val itemId = MutableLiveData<String>()
val result = itemId.switchMap {
    liveData { emit(fetchItem(it)) }
}

LiveData 協(xié)程構(gòu)造方法還可以接收一個 Dispatcher 作為參數(shù),這樣您就可以將這個協(xié)程移至另一個線程。


liveData(Dispatchers.IO) {
}

最后,您還可以使用 emitSource() 方法從另一個 LiveData 獲取更新的結(jié)果:


liveData(Dispatchers.IO) {
    emit(LOADING_STRING)
    emitSource(dataSource.fetchWeather())
}

接下來我們來看如何取消協(xié)程。絕大部分情況下,協(xié)程的取消操作是自動的,畢竟我們在對應(yīng)的作用域里啟動一個協(xié)程時,也同時明確了它會在何時被取消。但我們有必要講一講如何在協(xié)程內(nèi)部來手動取消協(xié)程。

這里補充一個大前提: 所有 kotlin.coroutines 的 suspend 方法都是可取消的。比如這種:


suspend fun printPrimes() {
    while(true) {
        // Compute
  delay(1000)
    }
}

在上面這個無限循環(huán)里,每一個 delay 都會檢查協(xié)程是否處于有效狀態(tài),一旦發(fā)現(xiàn)協(xié)程被取消,循環(huán)的操作也會被取消。

那問題來了,如果您在 suspend 方法里調(diào)用的是一個不可取消的方法呢?這時您需要使用 isActivate 來進(jìn)行檢查并手動決定是否繼續(xù)執(zhí)行操作:


suspend fun printPrimes() {
    while(isActive) {
        // Compute
    }
}

LiveData 操作實踐

在進(jìn)入具體的操作實踐環(huán)節(jié)之前,我們需要區(qū)分一下兩種操作: 單次 (One-Shot) 操作和監(jiān)聽 (observers) 操作。比如 Twitter 的應(yīng)用:

單次操作,比如獲取用戶頭像和推文,只需要執(zhí)行一次即可。監(jiān)聽操作,比如界面下方的轉(zhuǎn)發(fā)數(shù)和點贊數(shù),就會持續(xù)更新數(shù)據(jù)。

讓我們先看看單次操作時的內(nèi)容架構(gòu):

如前所述,我們使用 LiveData 連接 View 和 ViewModel,而在 ViewModel 這里我們則使用剛剛提到的 liveData 協(xié)程構(gòu)造方法來打通 LiveData 和協(xié)程,再往右就是調(diào)用 suspend 方法了。

如果我們想監(jiān)聽多個值的話,該如何操作呢?

第一種選擇是在 ViewModel 之外也使用 LiveData:

△ Reopsitory 監(jiān)聽 Data Source 暴露出來的 LiveData,同時自己也暴露出 LiveData 供 ViewModel 使用

但是這種實現(xiàn)方式無法體現(xiàn)并發(fā)性,比如每次用戶登出時,就需要手動取消所有的訂閱。LiveData 本身的設(shè)計并不適合這種情況,這時我們就需要使用第二種選擇: 使用 Flow。

ViewModel 模式

當(dāng) ViewModel 監(jiān)聽 LiveData,而且沒有對數(shù)據(jù)進(jìn)行任何轉(zhuǎn)換操作時,可以直接將 dataSource 中的 LiveData 賦值給 ViewModel 暴露出來的
LiveData:


val currentWeather: LiveData<String> =  
    dataSource.fetchWeather()

如果使用 Flow 的話就需要用到 liveData 協(xié)程構(gòu)造方法。我們從 Flow 中使用 collect 方法獲取每一個結(jié)果,然后 emit 出來給 liveData 協(xié)程構(gòu)造方法使用:


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

不過 Flow 給我們準(zhǔn)備了更簡單的寫法:


val currentWeatherFlow: LiveData<String> = 
    dataSource.fetchWeatherFlow().asLiveData()

接下來一個場景是,我們先發(fā)送一個一次性的結(jié)果,然后再持續(xù)發(fā)送多個數(shù)值:


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

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


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

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


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

接下來我們看看需要為接收到的數(shù)據(jù)做轉(zhuǎn)換時的情況。

使用 LiveData 時,如果用 map 方法做轉(zhuǎn)換,操作會進(jìn)入主線程,這顯然不是我們想要的結(jié)果。這時我們可以使用 switchMap,從而可以通過 liveData 協(xié)程構(gòu)造方法獲得一個 LiveData,而且 switchMap 的方法會在每次數(shù)據(jù)源 LiveData 更新時調(diào)用。而在方法體內(nèi)部我們可以使用 heavyTransformation 函數(shù)進(jìn)行數(shù)據(jù)轉(zhuǎn)換,并發(fā)送其結(jié)果給 liveData 協(xié)程構(gòu)造方法:


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

使用 Flow 的話會簡單許多,直接從 dataSource 獲得數(shù)據(jù),然后調(diào)用 map 方法 (這里用的是 Flow 的 map 方法,而不是 LiveData 的),然后轉(zhuǎn)化為 LiveData 即可:

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

Repository 模式

Repository 一般用來進(jìn)行復(fù)雜的數(shù)據(jù)轉(zhuǎn)換和處理,而 LiveData 沒有針對這種情況進(jìn)行設(shè)計?,F(xiàn)在通過 Flow 就可以完成各種復(fù)雜的操作:

val currentWeatherFlow: Flow<String> =
    dataSource.fetchWeatherFlow()
        .map { ... }
        .filter { ... }
        .dropWhile { ... }
        .combine { ... }
        .flowOn(Dispatchers.IO)
        .onCompletion { ... }
...

數(shù)據(jù)源模式
而在涉及到數(shù)據(jù)源時,情況變得有些復(fù)雜,因為這時您可能是在和其他代碼庫或者遠(yuǎn)程數(shù)據(jù)源進(jìn)行交互,但是您又無法控制這些數(shù)據(jù)源。這里我們分兩種情況介紹:

1. 單次操作
如果使用 Retrofit 從遠(yuǎn)程數(shù)據(jù)源獲取數(shù)值,直接將方法標(biāo)記為 suspend 方法即可*:


suspend fun doOneShot(param: String) : String =
    retrofitClient.doSomething(param)
  • Retrofit 從 2.6.0 開始支持 suspend 方法,Room 從 2.1.0 開始支持 suspend 方法。

如果您的數(shù)據(jù)源尚未支持協(xié)程,比如是一個 Java 代碼庫,而且使用的是回調(diào)機制。這時您可以使用 suspendCancellableCoroutine 協(xié)程構(gòu)造方法,這個方法是協(xié)程和回調(diào)之間的適配器,會在內(nèi)部提供一個 continuation 供開發(fā)者使用:


suspend fun doOneShot(param: String) : Result<String> =
    suspendCancellableCoroutine { continuation ->
        api.addOnCompleteListener { result ->
            continuation.resume(result)
        }.addOnFailureListener { error ->
            continuation.resumeWithException(error)
        }
  }

如上所示,在回調(diào)方法取得結(jié)果后會調(diào)用 continuation.resume(),如果報錯的話調(diào)用的則是 continuation.resumeWithException()。

注意,如果這個協(xié)程已經(jīng)被取消,則 resume 調(diào)用也會被忽略。開發(fā)者可以在協(xié)程被取消時主動取消 API 請求。

2. 監(jiān)聽操作
如果數(shù)據(jù)源會持續(xù)發(fā)送數(shù)值的話,使用 flow 協(xié)程構(gòu)造方法會很好地滿足需求,比如下面這個方法就會每隔 2 秒發(fā)送一個新的天氣值:


override fun fetchWeatherFlow(): Flow<String> = flow {
    var counter = 0
    while(true) {
        counter++
        delay(2000)
        emit(weatherConditions[counter % weatherConditions.size])
    }
}

如果開發(fā)者使用的是不支持 Flow 而是使用回調(diào)的代碼庫,則可以使用 callbackFlow。比如下面這段代碼,api 支持三個回調(diào)分支 onNextValue、onApiError 和 onCompleted,我們可以得到結(jié)果的分支里使用 offer 方法將值傳給 Flow,在發(fā)生錯誤的分支里 close 這個調(diào)用并傳回一個錯誤原因 (cause),而在順利調(diào)用完成后直接 close 調(diào)用:


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 徹底關(guān)閉這條數(shù)據(jù)通道。

相信看到這里,您對如何在實際應(yīng)用中使用協(xié)程、LiveData 和 Flow 已經(jīng)有了比較系統(tǒng)的認(rèn)識。您可以重溫 Android Dev Summit 上 Jose Alcérreca 和 Yigit Boyar 的演講來鞏固理解:

視頻鏈接:https://v.qq.com/x/page/a30225h40dj.html

點擊這里進(jìn)一步了解 LiveData

如果您對協(xié)程、LiveData 和 Flow 有任何疑問和想法,歡迎在評論區(qū)和我們分享。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容