從 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<String>()
val result: LiveData<String> = _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<String>()
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<String> =
dataSource.fetchWeather()
如果使用 Flow 的話就需要用到 liveData 協程構造方法。我們從 Flow 中使用 collect 方法擷取每一個結果,然後 emit 出來給 liveData 協程構造方法使用:
val currentWeatherFlow: LiveData<String> = liveData {
dataSource.fetchWeatherFlow().collect {
emit(it)
}
}
不過 Flow 給我們準備了更簡單的寫法:
val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow().asLiveData()
接下來一個場景是,我們先發送一個一次性的結果,然後再持續發送多個數值:
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()
接下來我們看看需要為接收到的資料做轉換時的情況。
使用 LiveData 時,如果用 map 方法做轉換,操作會進入主線程,這顯然不是我們想要的結果。這時我們可以使用 switchMap,進而可以通過 liveData 協程構造方法獲得一個 LiveData,而且 switchMap 的方法會在每次資料源 LiveData 更新時調用。而在方法體内部我們可以使用 heavyTransformation 函數進行資料轉換,并發送其結果給 liveData 協程構造方法:
val currentWeatherLiveData: LiveData<String> =
dataSource.fetchWeather().switchMap {
liveData { emit(heavyTransformation(it)) }
}
使用 Flow 的話會簡單許多,直接從 dataSource 獲得資料,然後調用 map 方法 (這裡用的是 Flow 的 map 方法,而不是 LiveData 的),然後轉化為 LiveData 即可:
val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow()
.map { heavyTransformation(it) }
.asLiveData()
Repository 模式 Repository 一般用來進行複雜的資料轉換和處理,而 LiveData 沒有針對這種情況進行設計。現在通過 Flow 就可以完成各種複雜的操作:
val currentWeatherFlow: Flow<String> =
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<String> =
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<String> = 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<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 徹底關閉這條資料通道。
相信看到這裡,您對如何在實際應用中使用協程、LiveData 和 Flow 已經有了比較系統的認識。您可以重溫 Android Dev Summit 上 Jose Alcérreca 和 Yigit Boyar 的演講來鞏固了解:
視訊連結:
v.qq.com/x/page/a302…如果您對協程、LiveData 和 Flow 有任何疑問和想法,歡迎在評論區和我們分享。
點選這裡進一步了解 LiveData
作者:Android_開發者
連結:
https://juejin.im/post/5ebb5c1ee51d454ddb0b4e1d來源:掘金