天天看點

android java協程,關于android:了解協程LiveData-和-Flow

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

△ Activity 生命周期流程圖

随着 Fragment 的退出,這個問題也變得更加簡單:

△ Fragment 生命周期流程圖

而開發者們面對這個挑戰,給出了十分持重的解決方案: 分層架構。

分層架構

△ 展現層 (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)。

協程的劣勢

協程的長處次要來自三個方面:

很容易來到主線程 。咱們試過很多辦法來讓操作遠離主線程,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)

但同樣的,這種狀況 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 的演講來 視訊 堅固了解。

如果您對協程、LiveData 和 Flow 有任何疑難和想法,歡送在評論區和咱們分享。