從 API 1 開始,處理 Activity 的生命週期 (lifecycle) 就是個老大難的問題,基本上開發者們都看過這兩張生命週期流程圖:html
△ Activity 生命週期流程圖android
隨着 Fragment 的加入,這個問題也變得更加複雜:git
△ Fragment 生命週期流程圖api
而開發者們面對這個挑戰,給出了很是穩健的解決方案: 分層架構。安全
△ 表現層 (Presentation Layer)、域層 (Domain Layer) 和數據層 (Data Layer)服務器
如上圖所示,經過將應用分爲三層,如今只有最上面的 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)。
協程的優勢主要來自三個方面:
在 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 } } }
看看咱們作了什麼:
這個作法並不理想。在 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 } }
在進入具體的操做實踐環節以前,咱們須要區分一下兩種操做: 單次 (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)
但一樣的,這種狀況 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 的演講來 視頻 鞏固理解。
若是您對協程、LiveData 和 Flow 有任何疑問和想法,歡迎在評論區和咱們分享。