Jetpack架構演變(一):初步使用flow,附加經典案例
在jetpack體系中 livedata的角色純純粹粹是個橋接器,DataSource中獲取到資料,然後由viewmodel進行邏輯處理,最後被livedata.postValue到view層,唯一的價值是綁定了lifecycle, 只在頁面活躍(start)的時候接受資料
官方的一篇介紹可以參考:從 LiveData 遷移到 Kotlin 資料流 - 掘金
對於初學者來說使用lieveData的好處是足夠簡單和相對安全
引入flow主要因為以下幾點:
- 具有更友好的API,學習成本較低
- 跟Kotlin協程、LiveData結合更緊密,Flow能夠轉換成LiveData,在ViewModel中直接使用
- 結合協程的作用域,當協程被取消時,Flow也會被取消,避免記憶體洩漏
- flow庫隸屬於kotlin, livedata屬於Android, 拜託Android平臺的限制對於未來跨平臺發展有利
【flow是個冷資料流】
所謂冷流,即下游無消費行為時,上游不會產生資料,只有下游開始消費,上游才開始產生資料。
而所謂熱流,即無論下游是否有消費行為,上游都會自己產生資料。
下邊通過一個經典場景詳細描述下flow(單純的flow,而stateFlow會在後續章節中講解)的使用
案例:一個菜譜應用app中,我想在一個頁面展示一個列表(recyclerview) ,此列表的每個item是個子列表,子列表依次為
計劃菜譜列表;
收藏菜譜列表;
根據食材篩選的菜譜列表;
根據食材獲取使用者偏好的菜譜列表;
如圖
四個子列表需要四個介面來獲取,組裝好後來重新整理最後的列表
其中每個列表都有可能是空,是emptylist的話這行就不顯示了,因為四個介面資料量大小不同,所以不會同一時間返回,同時又要保障這四個子列表按要求的順序來展示。
思路:
設計資料結構,最外層的data:
data class ContainerData(val title : String , val list: List<Recipe>)
其中Recipe實體是每個菜譜
data class Recipe(val id: String,
val name: String,
val cover: String,
val type: Int,
val ingredients: List<String>? = mutableListOf(),
val minutes: Int,
val pantryItemCount : Int )
模擬四個請求為:
val plannlist = Request.getPlannlist()
val favouritelist= Request.getFavouritelist()
... 以此類推
如果按照要求四個請求返回次序不同,同時要求在列表中按順序顯示,如果實現?
方案一:可以等待四個請求都返回後然後組裝資料,重新整理列表
可以利用協程的await方法:
```
val dataList = MutableLiveData>()
viewModelScope.launch { // planner val plannerDefer = async { Request.getPlannlist() }
// favourite val favouriteDefer = async { Request.getFavouritelist() }
val plannerData = plannerDefer.await() val favouriteData = favouriteDefer.await()
....省略後兩個介面
val list = listof( Container("planner" , plannerData), Container("favourite" , favouriteData), ... )
dataList.postValue(list)
}
```
await() 方法是掛起協程,四個介面非同步請求(非順序),等最後一個數據請求返回後才會執行下邊的步驟
然後組裝資料利用liveData傳送,在view中渲染
viewModel.dataList.observe(viewLifecycleOwner) {
mAdapter.submitList(it)
}
此種方式簡單,並且有效解決了按順序排列四個列表的需求,缺點是體驗差,假如有一個介面極慢,其他幾個就會等待它,使用者看著loading一直髮呆麼。
方案二:介面間不再互相等待,哪個介面先回來就渲染哪個,問題就是如何保障順序?
有的同學會有方案:先定製一個空資料list
val list = listOf(
Container("planner", emptylist()),
Container("favourite", emptylist()),
...
)
然後先用adapter去渲染list,哪個介面回來就去之前的列表查詢替換,然後adapter重新整理對應的資料,當然可以,不過會產生一部分邏輯膠水程式碼,查詢遍歷的操作。
此時我們可以藉助flow來實現了
1 構造一個planner資料流
val plannerFlow = flow {
val plannList = Request.getPlanlist()
emit(ContainerData("Planner", plannList))
}.onStart {
emit(ContainerData("", emptylist()))
}
注意是個val 變數, 不要寫成 fun plannerFlow() 方法,不然每次呼叫開闢新棧的時候新建個flow,並且會一直儲存在記憶體中,直到協程取消
其中onStart 會在傳送正式資料之前傳送,作為預載入。
然後我們就可以構造正式請求了
``` viewModelScope.launch {
combine(plannerFlow , favouriteFlow , xxxFlow ,xxxFlow) { planner , favourites , xxx , xxx ->
mutableListOf(planner , favourites , xxx,xxx)
}.collect {
datalist.postValue(it)
}
}
```
combine 的官方註釋為
Returns a Flow whose values are generated with transform function by combining the most recently emitted values by each flow.
combine操作符可以連線兩個不同的Flow , 一旦產生資料就會觸發組合後的flow的流動,同時它是有序的。
後續章節繼續講述flow其他特性,並徹底棄用liveData。