作者:HiDhl
鏈接:https://juejin.im/post/5f153adff265da22fb287e6e
我相信如今幾乎所有的 Android 開發(fā)者至少都聽過 MVVM 架構(gòu),在 Google Android 團(tuán)隊(duì)宣布了 Jetpack 的視圖模型之后,它已經(jīng)成為了現(xiàn)代 Android 開發(fā)模式最流行的架構(gòu)之一,如下圖所示:

在官宣 Jetpack 的視圖模型之后,同時 Google 在 Jetpack Guide 文章中的示例,也在 Repositories 或者 DataSource 中使用 LiveData,以至于在很多開源的 MVVM 項(xiàng)目中也是直接使用 LiveData,但是在 Repositories 或者 DataSource 中直接使用 LiveData 這種做法對嗎?這是我一直以來的一個疑問?
直到我打開 Android 架構(gòu)組件 頁面,看了在頁面上增加了最新的文章,這幾篇文章大概的內(nèi)容是說如何在 MVVM 中使用 Flow 以及如何與 LiveData 一起使用,當(dāng)我看完并通過實(shí)踐之后大概明白了,LiveData 是一個生命周期感知組件,它并不屬于 Repositories 或者 DataSource 層,下文會有詳細(xì)的分析。

在 Google 發(fā)布的 Jetpack 的最新成員 Paging3,在其內(nèi)部的源碼實(shí)現(xiàn)也是使用的 Flow,關(guān)于 Paging3 的使用可以參考以下鏈接:
- Jetpack 成員 Paging3 實(shí)踐以及源碼分析(一)
- Jetpack 新成員 Paging3 網(wǎng)絡(luò)實(shí)踐及原理分析(二)
- 自定義 MediatorResult 實(shí)現(xiàn) network + db 的混合使用
不僅僅是 Jetpack 成員支持 Flow,在 Google 提供的 Demo 里面也都在使用 Flow,也有很多開源的 MVVM 項(xiàng)目也在逐漸切換到 Flow,為什么 Google 會推薦使用它呢,使用 Flow 能帶來那些好處呢,為我們解決了什么問題?
Kotlin Flow 是什么?Kotlin Flow 解決了什么問題?
Flow 庫是在 Kotlin Coroutines 1.3.2 發(fā)布之后新增的庫,也叫做異步流,類似 RxJava 的 Observable 、 Flowable 等等,所以很多人都用 Flow 與 RxJava 做對比。
Flow 相比于 RxJava 簡單的太多了,你還記得那些 RxJava 傻傻分不清楚的操作符嗎 Observable 、 Flowable 、 Single 、 Completable 、 Maybe 等等。
那么 Flow 為我們解決了什么問題,我主要從以下幾個方面思考:
-
LiveData 是一個生命周期感知組件,最好在 View 和 ViewModel 層中使用它,如果在 Repositories 或者 DataSource 中使用會有幾個問題
- 它不支持線程切換,其次不支持背壓,也就是在一段時間內(nèi)發(fā)送數(shù)據(jù)的速度 > 接受數(shù)據(jù)的速度,LiveData 無法正確的處理這些請求
- 使用 LiveData 的最大問題是所有數(shù)據(jù)轉(zhuǎn)換都將在主線程上完成
RxJava 雖然支持線程切換和背壓,但是 RxJava 那么多傻傻分不清楚的操作符,實(shí)際上在項(xiàng)目中常用的可能只有幾個例如
Observable、Flowable、Single等等,如果我們不去了解背后的原理,造成內(nèi)存泄露是很正常的事,大家可以從 StackOverflow 上查看一下,有很多因?yàn)?RxJava 造成內(nèi)存泄露的例子RxJava 入門的門檻很高,學(xué)習(xí)過的朋友們,我相信能夠體會到從入門到放棄是什么感覺
解決回調(diào)地獄的問題
而相對于以上的不足,F(xiàn)low 有以下優(yōu)點(diǎn):
- Flow 支持線程切換、背壓
- Flow 入門的門檻很低,沒有那么多傻傻分不清楚的操作符
- 簡單的數(shù)據(jù)轉(zhuǎn)換與操作符,如 map 等等
- Flow 是對 Kotlin 協(xié)程的擴(kuò)展,讓我們可以像運(yùn)行同步代碼一樣運(yùn)行異步代碼,使得代碼更加簡潔,提高了代碼的可讀性
- 易于做單元測試
Kotlin Flow 如何在 MVVM 中使用
Jetpack 的視圖模型 MVVM 架構(gòu)由 View + DataBinding + ViewModel + Model 組成,如下所示,我相信下面這張圖大家非常熟悉了,

接下來我們一起來探究一下 Kotlin Flow 在 MVVM 當(dāng)中每層是如何實(shí)現(xiàn)的。
Kotlin Flow 在數(shù)據(jù)源中的使用
在 PokemonGo 項(xiàng)目中,進(jìn)入詳情頁,會檢查本地是否有數(shù)據(jù),如果沒有會去請求 pokeapi 詳情頁接口,獲得最新的數(shù)據(jù),然后存儲在數(shù)據(jù)庫中。
Flow 是協(xié)程的擴(kuò)展,如果要在 Room 和 Retrofit 中使用,Room 和 Retrofit 需要支持協(xié)程才可以,在 Retrofit >= 2.6.0 和 Room >= 2.1 版本都支持協(xié)程,我們來看一下 Room 和 Retrofit 數(shù)據(jù)源的配置。
Room:
PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/local/PokemonInfoDao.kt
@Query("SELECT * FROM PokemonInfoEntity where name = :name")
suspend fun getPokemon(name: String): PokemonInfoEntity?
復(fù)制代碼
或者直接返回 Flow<PokemonInfoEntity>
@Query("SELECT * FROM PokemonInfoEntity where name = :name")
fun getPokemon(name: String): Flow<PokemonInfoEntity>
復(fù)制代碼
Retrofit:
PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/remote/PokemonService.kt
@GET("pokemon/{name}")
suspend fun fetchPokemonInfo(@Path("name") name: String): NetWorkPokemonInfo
復(fù)制代碼
如上所見在方法前增加了用 suspend 進(jìn)行了修飾,只有被 suspend 修飾的方法,才可以在協(xié)程中調(diào)用。
按照如上配置,在數(shù)據(jù)源的工作就完成了,相比于 RxJava 的 Observable 、 Flowable 、 Single 、 Completable 、 Maybe 使用場景要簡單太多了,我們來看一下在 Repositories 中是如何使用的。
Kotlin Flow 在 Repositories 中的使用
如果我們想在 Flow 中使用 Retrofit 或者 Room 進(jìn)行網(wǎng)絡(luò)請求或者查詢數(shù)據(jù)庫的操作,我們需要將使用 suspend 修飾符的操作放到 flow { ... } 中執(zhí)行,最后使用 emit() 方法更新數(shù)據(jù),將數(shù)據(jù)發(fā)送給 ViewModel,代碼如下所示:
PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/repository/PokemonRepositoryImpl.kt
flow {
val pokemonDao = db.pokemonInfoDao()
// 查詢數(shù)據(jù)庫是否存在,如果不存在請求網(wǎng)絡(luò)
var infoModel = pokemonDao.getPokemon(name)
if (infoModel == null) {
// 網(wǎng)絡(luò)請求
val netWorkPokemonInfo = api.fetchPokemonInfo(name)
// 將網(wǎng)路請求的數(shù)據(jù),換轉(zhuǎn)成的數(shù)據(jù)庫的 model,之后插入數(shù)據(jù)庫
infoModel = netWorkPokemonInfo.let {
PokemonInfoEntity(
name = it.name,
height = it.height,
weight = it.weight,
experience = it.experience
)
}
// 插入更新數(shù)據(jù)庫
pokemonDao.insertPokemon(infoModel)
}
// 將數(shù)據(jù)源的 model 轉(zhuǎn)換成上層用到的 model,
// ui 不能直接持有數(shù)據(jù)源,防止數(shù)據(jù)源的變化,影響上層的 ui
val model = mapper2InfoModel.map(infoModel)
// 更新數(shù)據(jù),將數(shù)據(jù)發(fā)送給 ViewModel
emit(model)
}.flowOn(Dispatchers.IO) // 通過 flowOn 切換到 IO 線程
復(fù)制代碼
將上面的代碼簡化如下所示:
flow {
// 進(jìn)行網(wǎng)絡(luò)或者數(shù)據(jù)庫操作
emit(model)
}.flowOn(Dispatchers.IO) // 通過 flowOn 切換到 IO 線程
復(fù)制代碼
正如你所見,將耗時操作放到 flow { ... } 里面,通過 flowOn(Dispatchers.IO) 切換到 IO 線程,最后通過 emit() 方法將數(shù)據(jù)發(fā)送給 ViewModel,接下來我們來看一下如何在 ViewModel 中接受 Flow 發(fā)送的數(shù)據(jù)。
Kotlin Flow 在 ViewModel 中的使用
在 ViewModel 中使用 Flow 之前在 Jetpack 成員 Paging3 實(shí)踐以及源碼分析(一) 文章也有提到, 這里我們在深入分析一下,在 ViewModel 中接受 Flow 發(fā)送的數(shù)據(jù)有三種方法,根據(jù)實(shí)際情況去調(diào)用。
PokemonGo/app/src/main/java/com/hi/dhl/pokemon/ui/detail/DetailViewModel.kt
方法一
在 LifeCycle 2.2.0 之前使用的方法,使用兩個 LiveData,一個是可變的,一個是不可變的,如下所示:
// 私有的 MutableLiveData 可變的,對內(nèi)訪問
private val _pokemon = MutableLiveData<PokemonInfoModel>()
// 對外暴露不可變的 LiveData,只能查詢
val pokemon: LiveData<PokemonInfoModel> = _pokemon
viewModelScope.launch {
polemonRepository.featchPokemonInfo(name)
.onStart {
// 在調(diào)用 flow 請求數(shù)據(jù)之前,做一些準(zhǔn)備工作,例如顯示正在加載數(shù)據(jù)的進(jìn)度條
}
.catch {
// 捕獲上游出現(xiàn)的異常
}
.onCompletion {
// 請求完成
}
.collectLatest {
// 將數(shù)據(jù)提供給 Activity 或者 Fragment
_pokemon.postValue(it)
}
}
復(fù)制代碼
- 準(zhǔn)備一私有的 MutableLiveData,只對內(nèi)訪問
- 對外暴露不可變的 LiveData
- 在
viewModelScope.launch方法中執(zhí)行協(xié)程代碼塊 -
collectLatest是末端操作符,收集 Flow 在 Repositories 層發(fā)射出來的數(shù)據(jù),在一段時間內(nèi)發(fā)送多次數(shù)據(jù),只會接受最新的一次發(fā)射過來的數(shù)據(jù) - 調(diào)用
_pokemon.postValue方法將數(shù)據(jù)提供給 Activity 或者 Fragment
方法二
在 LifeCycle 2.2.0 之后,可以用更精簡的方法來完成,使用 LiveData 協(xié)程構(gòu)造方法 (coroutine builder),這個方法也是在 PokemonGo 項(xiàng)目中用到的方法。
@OptIn(ExperimentalCoroutinesApi::class)
fun fectchPokemonInfo(name: String) = liveData<PokemonInfoModel> {
polemonRepository.featchPokemonInfo(name)
.onStart { // 在調(diào)用 flow 請求數(shù)據(jù)之前,做一些準(zhǔn)備工作,例如顯示正在加載數(shù)據(jù)的進(jìn)度條 }
.catch { // 捕獲上游出現(xiàn)的異常 }
.onCompletion { // 請求完成 }
.collectLatest {
// 更新 LiveData 的數(shù)據(jù)
emit(it)
}
}
復(fù)制代碼
-
liveData{ ... }協(xié)程構(gòu)造方法提供了一個協(xié)程代碼塊,產(chǎn)生的是一個不可變的 LiveData,emit()方法則用來更新 LiveData 的數(shù)據(jù) -
collectLatest是末端操作符,收集 Flow 在 Repositories 層發(fā)射出來的數(shù)據(jù),在一段時間內(nèi)發(fā)送多次數(shù)據(jù),只會接受最新的一次發(fā)射過來的數(shù)據(jù)
方法三:
調(diào)用 Flow 的擴(kuò)展方法 asLiveData() 返回一個不可變的 LiveData,供 Activity 或者 Fragment 調(diào)用。
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun fectchPokemonInfo3(name: String) =
polemonRepository.featchPokemonInfo(name)
.onStart {
// 在調(diào)用 flow 請求數(shù)據(jù)之前,做一些準(zhǔn)備工作,例如顯示正在加載數(shù)據(jù)的按鈕
}
.catch {
// 捕獲上游出現(xiàn)的異常
}
.onCompletion {
// 請求完成
}.asLiveData()
復(fù)制代碼
因?yàn)?polemonRepository.featchPokemonInfo(name) 是一個用 suspend 修飾的方法,所以在 ViewModel 中調(diào)用也需要使用 suspend 來修飾。
為什么說調(diào)用 asLiveData() 方法會返回一個不可變的 LiveData,我們來看一下源碼:
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}
復(fù)制代碼
asLiveData() 方法其實(shí)就是對 方法二 中的 liveData{ ... } 的封裝
-
asLiveData是 Flow 的擴(kuò)展函數(shù),返回值是一個 LiveData -
liveData{ ... }協(xié)程構(gòu)造方法提供了一個協(xié)程代碼塊,在liveData{ ... }中執(zhí)行協(xié)程代碼 -
collect是末端操作符,收集 Flow 在 Repositories 層發(fā)射出來的數(shù)據(jù) - 最后調(diào)用 LiveData 中的
emit()方法更新 LiveData 的數(shù)據(jù)
DataBinding(數(shù)據(jù)綁定)
在 PokemonGo 項(xiàng)目中使用了 DataBinding 進(jìn)行的數(shù)據(jù)綁定。
DataBinding(數(shù)據(jù)綁定)實(shí)際上是 XML 布局中的另一個視圖結(jié)構(gòu)層次,視圖 (XML) 通過數(shù)據(jù)綁定層不斷地與 ViewModel 交互,如下所示:
PokemonGo/app/src/main/res/layout/activity_details.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.hi.dhl.pokemon.ui.detail.DetailViewModel" />
</data>
......
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/weight"
android:text="@{viewModel.pokemon.getWeightString}"/>
......
</layout>
復(fù)制代碼
這是獲取神奇寶貝的詳細(xì)信息,通過 DataBinding 以聲明方式將數(shù)據(jù)(神奇寶貝的體重)綁定到界面上,更多使用參考項(xiàng)目中的代碼。
如何處理 ViewModel 的三種方式
如果不使用數(shù)據(jù)綁定,在 Activity 或者 Fragment 中如何處理 ViewModel 的三種方式。
PokemonGo/app/src/main/java/com/hi/dhl/pokemon/ui/detail/DetailActivity.kt
方式一:
使用兩個 LiveData,一個是可變的,一個是不可變的,在 Activity 或者 Fragment 中調(diào)用對外暴露不可變的 LiveData 即可,如下所示:
// 方法一
mViewModel.pokemon.observe(this, Observer {
// 將數(shù)據(jù)顯示在頁面上
})
復(fù)制代碼
方式二:
使用 LiveData 協(xié)程構(gòu)造方法 (coroutine builder) 提供的協(xié)程代碼塊,產(chǎn)生的是一個不可變的 LiveData,處理方式 同方法一,在 Activity 或者 Fragment 中調(diào)用這個不可變的 LiveData 即可,如下所示:
// 方法二
mViewModel.fectchPokemonInfo2(mPokemonModel.name).observe(this, Observer {
// 將數(shù)據(jù)顯示在頁面上
})
復(fù)制代碼
方式三:
調(diào)用 Flow 的擴(kuò)展方法 asLiveData() 返回一個不可變的 LiveData,在 Activity 或者 Fragment 調(diào)用這個不可變的 LiveData 即可,如下所示:
// 方法三
lifecycleScope.launch {
mViewModel.apply {
fectchPokemonInfo3(mPokemonModel.name).observe(this@DetailActivity, Observer {
// 將數(shù)據(jù)顯示在頁面上
})
}
}
復(fù)制代碼
到這里關(guān)于 Kotlin Flow 在 MVVM 當(dāng)中每層的實(shí)踐就分析完了,如果使用過 RxJava 的小伙伴們應(yīng)該會非常熟悉,對于沒有使用過 RxJava 的小伙伴們,入門的門檻也是非常低的,強(qiáng)烈建議至少體驗(yàn)一次,體驗(yàn)過之后,我認(rèn)為你會跟我一樣愛上它的。
神奇寶貝 (PokemonGo) 基于 Jetpack + MVVM + Repository + Data Mapper + Kotlin Flow 的實(shí)戰(zhàn)項(xiàng)目,我也正在為 PokemonGo 項(xiàng)目設(shè)計(jì)更多的場景,也會加入更多的 Jetpack 成員,可以點(diǎn)擊下方鏈接前往查看。
PokemonGo GitHub 地址:https://github.com/hi-dhl/PokemonGo

<figcaption></figcaption>
結(jié)語
整理了一份《高級Kotlin強(qiáng)化實(shí)戰(zhàn)(附Demo)》,內(nèi)容涵蓋 Kotlin入門教程、Kotlin實(shí)戰(zhàn)避坑指南、Kotlin Jetpack 實(shí)戰(zhàn)三大模塊。

詳細(xì)文檔可以點(diǎn)我下載,記得點(diǎn)贊哦~