原文地址 https://blog.csdn.net/mjlong1...
背景
flow簡單的可以理解為數據流,它可以生成連續的同類型數據。剛接觸到flow的開發者都很疑惑,它的功能好像都有東西可以替代。比如通過foreach遍歷Collection或Sequence都能有flow一樣的生成數據效果,那為什麼還要引入flow呢。大家可能會認為flow實現了觀察者模式,這點與collection或sequence的遍歷不同。其實LiveData就是按照觀察者模式設計的,LiveData配合集合的遍歷就可以達到數據被觀察的目的。
剛接觸flow時想理解它的本質目的確實有點費勁,但是經過簡單的實踐後我們發現他的優勢表現在與協程的配合上。大家想一想Collection或是sequence的操作支持掛起嗎?答案是否定的,它們不支持。但是flow的操作都是掛起函數,用户可以在flow的不同操作中調用其他的掛起函數,並且flow還可以通過flowon來切換flow所運行的協程。
flow 介紹
flow特質:
在協程中與產生一條數據的掛起函數比,flow可以有序生成多條數據。
與生成多條數據的Iterator相比,flow在數據生成的過程中可以調用掛起函數異步生成數據,同時不會阻塞當前線程。
生成的數據序列是同類型的數據。
flow中的三個角色:
數據的生成者=》可以通過掛起函數異步生產一系列數據。
中介者=》可以對生成的數據進行修改。
數據的消費者=》使用生成的數據,一般用户界面展示。
flow{// 生成1,2,3數據序列
emit(1)
emit(2)
emit(3)
}.map {
value -> value * 2 //修改數據
}.collect {
result-> println(result) //顯示轉換過的數據
}
flow加載列表數據
Android應用加載列表數據是一個比較普遍的需求,我們如何使用flow實現列表數據的加載和顯示呢?
首先我們先分析下再加載列表數據都需要處理哪些問題:
- 加載數據過程中顯示loading,數據加載完成隱藏loading。
flow {
val ret = serverApi.getList(requestId)
emit(ret)
}.onStart {
progressBar.visibility = View.VISIBLE
}.onCompletion {
progressBar.visibility = View.INVISIBLE
}.collect()
onStart在數據流開始收集的時候被調用,onCompletion在數據流結束時被調用。這裏面數據是通過掛起方法getList生成的單一數據,所以這個數據流生成一條數據後就結束了。我們可以發現這裏通過數據流的鏈式處理再配合協程的掛起函數,我們可以避免異步回調的使用。
2.當加載的數據為空時顯示空畫面。
flow {
val ret = serverApi.getList(requestId)
if (ret.isNotEmpty()) {
emit(ret)
}
}.onStart {
progressBar.visibility = View.VISIBLE
}.onEmpty {
loadDataRetryButton.visibility = View.VISIBLE
}.onCompletion {
progressBar.visibility = View.INVISIBLE
}.collect()
onEmpty在數據為空時被調用,那什麼情況是數據為空呢?其實數據流的數據為空只的是數據流被收集時,數據流沒有生成任何數據,在這裏就是沒有調用emit發射任何數據的時候。我們可以看到ret.isNotEmpty的判斷,只有數據不為空時才進行發射,數據為空時沒有發射任何數據,這時onEmpty被調用。
3.獲取數據過程中發送異常時,我們需要顯示異常畫面。
flow {
val ret = serverApi.getList(requestId)
if (ret.isNotEmpty()) {
emit(ret)
}
}.onStart {
progressBar.visibility = View.VISIBLE
}.onEmpty {
loadDataRetryButton.visibility = View.VISIBLE
}.catch {
msgTextView.visibility = View.VISIBLE
msgTextView.text = "發送異常"
loadDataRetryButton.visibility = View.VISIBLE
}.onCompletion {
progressBar.visibility = View.INVISIBLE
}.collect()
catch在數據流生成過程中發生異常的時候被調用,我們在catch塊中顯示錯誤信息。有一點需要注意,catch塊寫的位置直接影響了捕獲異常的範圍。在flow的鏈式調用中,catch塊只會捕獲鏈式調用中它前面的處理產生的異常。
4.顯示flow生成的列表數據
flow {
val ret = serverApi.getList(requestId)
if (ret.isNotEmpty()) {
emit(ret)
}
}.onStart {
progressBar.visibility = View.VISIBLE
}.onEmpty {
loadDataRetryButton.visibility = View.VISIBLE
}.onEach {
adapter.setData(it)
adapter.notifyDataSetChanged()
}.catch {
msgTextView.visibility = View.VISIBLE
msgTextView.text = "發送異常"
loadDataRetryButton.visibility = View.VISIBLE
}.onCompletion {
progressBar.visibility = View.INVISIBLE
}.collect{
print(it)
}
onEach在每條數據被髮射後會被調用,我們可以在這裏接收並顯示數據。當然我們也可以在collect中顯示數據,但是onEach有個優勢,它可以寫在catch塊前面,這樣onEach中產生的異常也可以被catch塊捕獲,collect就沒有這樣的優勢。
5.在網絡數據獲取失敗的情況下使用本地緩存的數據。
flow {
val ret = serverApi.getList(requestId)
if (ret.isNotEmpty()) {
emit(ret)
}
}.onStart {
progressBar.visibility = View.VISIBLE
}.onEmpty {
loadDataRetryButton.visibility = View.VISIBLE
}.catch {
if (cacheList.isEmpty()) {
msgTextView.text = "發生異常"
loadDataRetryButton.visibility = View.VISIBLE
} else {
emit(cacheList)
}
}.onEach {
cacheList = cacheList
adapter.setData(it)
adapter.notifyDataSetChanged()
}.catch{
msgTextView.text = "onEach異常"
loadDataRetryButton.visibility = View.VISIBLE
}.onCompletion {
progressBar.visibility = View.INVISIBLE
}.collect{
print(it)
}
在onEach塊中我們把成功獲取的數據進行保存,然後在catch塊中我們判斷是否有緩存數據,如果有緩存數據則向下遊發射。這裏需要注意的是catch塊中調用emit發射的數據只能被鏈式調用的catch塊後面的操作接收到。這裏大家可能要問,在onEach中發射的異常我們如何捕獲?其實在鏈式操作中,所有的操作都可以使用多次,所以我們可以在onEach塊後追加一個catch塊來捕獲onEach中發生的異常。
6.數據獲取和處理的過程中可以方便的切換線程,掛起線程而不是阻塞線程。
var listDataFlow= flow {
val ret = serverApi.getList(requestId)
if (ret.isNotEmpty()) {
emit(ret)
}
}flowOn(Dispatchers.IO)
.onStart {
progressBar.visibility = View.VISIBLE
}.onEmpty {
loadDataRetryButton.visibility = View.VISIBLE
}.catch {
if (cacheList.isEmpty()) {
msgTextView.text = "發生異常"
loadDataRetryButton.visibility = View.VISIBLE
} else {
emit(cacheList)
}
}.onEach {
cacheList = cacheList
adapter.setData(it)
adapter.notifyDataSetChanged()
}.catch{
msgTextView.text = "onEach異常"
loadDataRetryButton.visibility = View.VISIBLE
}.onCompletion {
progressBar.visibility = View.INVISIBLE
}
lifecycleScope.launch { listDataFlow.collect() }
getList方法是耗時方法,通常需要異步線程配合回調函數來處理。flow支持掛起方法調用,所以這裏的getList方式被聲明成suspend方法,然後通過flowOn方法切換到IO線程執行getList方法。flowOn隻影響鏈式調用中它前面的方法的執行線程,對後面的方法執行線程沒有影響。那麼後面的方法執行在哪個線程呢?答案是後面的方法執行在收集方法collect被調用的線程。這裏啓動協程時沒有指定線程,所以它執行在Android的主線程中。
總結
使用flow的方式加載列表數據時有下面幾個特點:
flow的鏈式調用替代了異步回調的方式,代碼簡潔易懂,避免了異步回調反覆嵌套的問題。
使用flowOn方法可以方便靈活地進行線程切換,並且在flow操作中都支持掛起方法,flow可以無縫對接協程。
flow處理過程是聲明式的,只有flow被收集的時候這些聲明的過程才被執行。聲明式的過程還有個好處是我們可以基於已有的flow聲明再追加新的處理過程聲明。
這篇文章以最簡單的方式展示了flow加載列表數據的流程,在實際應用中肯定要更復雜些。這裏的flow聲明都在fragment中,實際應用中還要進行基本的分層處理。flow的聲明屬於DataSource層的。在flow向上傳遞的過程中,我們可以為底層的flow聲明新的處理,比如在repository層追加聲明本地緩存處理,在viewmodel層追加聲明ui狀態更新處理等。本質就是將例子中的處理分解到不同層次上進行追加聲明。
我的公眾號已經開通,公眾號會同步發佈。
歡迎關注我的公眾號