Jetpack架構演變(一):初步使用flow,附加經典案例

語言: CN / TW / HK

在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。