博客 / 詳情

返回

Android app中這樣用flow更方便-加載列表數據

原文地址 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實現列表數據的加載和顯示呢?

        首先我們先分析下再加載列表數據都需要處理哪些問題:

  1. 加載數據過程中顯示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狀態更新處理等。本質就是將例子中的處理分解到不同層次上進行追加聲明。

        我的公眾號已經開通,公眾號會同步發佈。
歡迎關注我的公眾號

user avatar var 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.