Kotlin之Flow由淺入深,對比Rxjava

原文鏈接

  1. sequence

    sequence又被稱爲惰性集合操作,下面舉例說明

    fun main() {
        val sequence = sequenceOf(1, 2, 3, 4)
        val result: Sequence<Int> = sequence.map { i ->
            println("Map $i")
            i * 2
        }.filter { i ->
            println("Filter $i")
            i % 3 == 0
        }
        println(result.first())//
    }
    

    執行結果如下

    Map 1
    Filter 2
    Map 2
    Filter 4
    Map 3
    Filter 6
    6
    

    惰性的概念首先說是在println(result.first())之前,關於result的map和filter都不會執行,只有result被使用的時候纔會執行,而且執行是惰性的,即map取出一個元素交給filter,而不是map對所有元素都處理過戶再交給filter,而且當滿足條件後就不會往下執行,由結果可以看出,沒有對Sequence的4進行map和filter操作,因爲3已經滿足了條件

    而List是沒有惰性的

    fun main() {
        val sequence = listOf<Int>(1, 2, 3, 4)
        val result: List<Int> = sequence.map { i ->
            println("Map $i")
            i * 2
        }.filter { i ->
            println("Filter $i")
            i % 3 == 0
        }
        println(result.first())
    }
    

    執行結果

    Map 1
    Map 2
    Map 3
    Map 4
    Filter 2
    Filter 4
    Filter 6
    Filter 8
    6
    

    List是聲明後立即執行,處理流程如下

    • {1,2,3,4}->{2,4,6,8}
    • 遍歷判斷是否能被3整除

    由此可以總結出Sequence的優勢

    • 一旦滿足遍歷需要退出的條件,就可以省略後面不必要的遍歷
    • 像List這種實現Iterable接口的集合類,每調用一次函數就會生成一個新的Iterable,下一個函數再基於新的Iterable執行,每次函數調用產生的臨時Iterable會導致額外的內存消耗,而Sequence在整個流程只有一個,且不改變原sequence
    • 因此,Sequence這種數據類型可以在數據量較大或數據量未知的時候,作爲流式處理的解決方案

    但是由上也可以看出Squence是同步完成這些操作的,那有沒有辦法使用異步完成那些map和filter等操作符呢,答案就是Flow

  2. Flow

    在這裏插入圖片描述

    如果我們把list轉成Flow並在最後調用collect{},會產生一個編譯錯誤,這是因爲Flow是基於協程構建的,默認具有異步功能,因此你只能在協程裏使用它,Flow也是cold stream,也就是直到你調用terminal operator(比如collect{}),Flow纔會執行,而且如果重複調用collect,則調用會得到同樣的結果

    Flow提供了很多操作符,比如map,filter,scan,groupBy等,它們都是cold stream的,我們可以利用這些操作符完成異步代碼。假如我們想使用map操作符來執行一些耗時任務(這裏我們用延遲來模擬耗時),在Rxjava中你可以使用flatmap來進行一些邏輯,比如

    fun main() {
        val  disposable = Observable.fromArray("A","Ba","ora","pear","fruit")
            .flatMap {string->//flatMap是異步,map是同步
                println("flatMap $string")
                Observable.just(string)
                    .delay(1000,TimeUnit.MILLISECONDS)
                    .map { it.length }//這裏是異步,對"A","Ba","ora","pear","fruit"異步執行轉換操作,因此下面也不是順序打印
            }
            .subscribe{
                print("subscribe:::")
                println(it)
            }
        Thread.sleep(10012)
    }
    

    執行結果

    flatMap A
    flatMap Ba
    flatMap ora
    flatMap pear
    flatMap fruit
    subscribe:::1
    subscribe:::5
    subscribe:::2
    subscribe:::3
    subscribe:::4
    

    使用Flow完成類似上面的操作是這樣的

    fun main() {
        runBlocking {
            flowOf("A","Ba","ora","pear","fruit")
                .map { stringToLength(it) }
                .collect { println(it) }//會依次打印 12345
        }
    }
    private suspend fun stringToLength(it:String):Int{
        delay(1000)
        return it.length
    }
    
  3. terminal operator

    上面提到collect()是terminal operator,意思就是僅當你調用它的時候纔會去得到結果,和sequence使用的時候纔會執行,Rxjava調用subscribe後纔會執行,Flow中的terminal operator是suspend函數,其他的terminal operator有toList,toSet;first(),reduce(),flod()等

  4. 取消 Cancellation

    每次設置Rxjava訂閱時,我們都必須考慮合適取消這些訂閱,以免發生內存溢出或者,在生命週期結束後依然在後臺執行任務(expired task working in background),調用subscribe後,Rxjava會s給我們返回一個Disposable對象,在需要時利用它的disposable方法可以取消訂閱,如果你有多個訂閱,你可以把這些訂閱放在CompositeDisposable,並在需要的時候調用它的clear()方法或者dispose方法

    val compositeDisposable = CompositeDisposable()
    compositeDisposable.add(disposable)
    compositeDisposable.clear()
    compositeDisposable.dispose()
    

    但是在協程作用內你完全不用考慮這些,因爲只會在作用域內執行,作用域外會自動取消

  5. Errors 處理異常

    Rxjava最有用的功能之一是處理錯誤的方式,你可以在onError裏處理所有的異常。同樣Flow有類似的方法catch{},如果你不使用此方法,那你的應用會拋出異常或者崩潰,你可以像之前一樣使用try catch或者catch{}來處理錯誤,下面讓我們來模擬一些錯誤

    fun main() {
        runBlocking {
            flowOfAnimeCharacters()
                .map { stringToLength(it)}
                .collect { println(it) }
        }
    }
    private fun flowOfAnimeCharacters() = flow {
        emit("Madara")
        emit("Kakashi")
        //throwing some error
        throw IllegalStateException()
        emit("Jiraya")
        emit("Itachi")
        emit("Naruto")
    }
    private suspend fun stringToLength(it:String):Int{
        delay(1000)
        return it.length
    }
    

    如果你運行了這個代碼,那麼程序顯然會拋出異常並在控制檯打印,下面我們分別使用上述的兩種方式處理異常

    fun main() {
        //使用try catch
        runBlocking {
           try {
               flowOfAnimeCharacters()
                   .map { stringToLength(it)}
                   .collect { println(it) }
           }catch (e:Exception){
               println(e.stackTrace)//雖然有異常,但是我們對異常做了處理,不會導致應用崩潰
           }finally {
                println("Beat it")
           }
        }
    //------------------------------------
    @ExperimentalCoroutinesApi
    fun main() {
        //using catch{}
        runBlocking {
            flowOfAnimeCharacters()
                .map { stringToLength(it)}
                .catch { println(it) }//不過這個好像還是實驗性質的api,不在方法上使用註解會警告,並且catch{}必須凡在terminal operator之前
                .collect { println(it) }
        }
    }
    
  6. resume 恢復

    如果代碼在執行過程中出現了異常,我們希望使用默認數據或者完整的備份來恢復數據流,在Rxjava中我們可以是使用 onErrorResumeNext()或者 onErrorReturn(),在Flow中我們依然可以使用catch{},但是我們需要在catch{}代碼塊裏使用emit()來一個一個的發送備份數據,甚至如果我們願意,可以使用emitAll()可以產生一個新的Flow,

    @ExperimentalCoroutinesApi
    fun main() {
        //using catch{}
        runBlocking {
          flowOfAnimeCharacters()
              .catch { emitAll(flowOf("Minato", "Hashirama")) }
              .collect { println(it) }
        }
    

    現在你可以得到如下結果

    Madara
    Kakashi
    Minato
    Hashirama
    
  7. flowOn()

    默認情況下Flow數據會運行在調用者的上下文(線程)中,如果你想隨時切換線程比如像Rxjava的observeOn(),你可以使用flowOn()來改變上游的上下文,這裏的上游是指調用flowOn之前的所有操作符,官方文檔有很好的說明

    /**
     * Changes the context where this flow is executed to the given [context].--改變Flow執行的上下文
     * This operator is composable and affects only preceding operators that do not have its own context.---這個操作符是可以多次使用的,它僅影響操作符之前沒有自己上下文的操作符
     * This operator is context preserving: [context] **does not** leak into the downstream flow.--這個操作符指定的上下文不會污染到下游,它會保留默認的上下文,例如下面例子中最後的操作符single()使用的是默認的上下文而不是上游指定的Dispatchers.Default
     *
     * For example:
     *
     * ```
     * withContext(Dispatchers.Main) {
     *     val singleValue = intFlow // will be executed on IO if context wasn't specified before
     *         .map { ... } // Will be executed in IO
     *         .flowOn(Dispatchers.IO)
     *         .filter { ... } // Will be executed in Default
     *         .flowOn(Dispatchers.Default)
     *         .single() // Will be executed in the Main
     * }
     * ```
    
  8. Completion

    當Flow完成發送是數據時,無論是成功或者失敗,你都想做一些事情,onCompletion()可以幫助你做到這一點:如下

    @ExperimentalCoroutinesApi
    fun main() {
        //using catch{}
        runBlocking {
          flowOfAnimeCharacters()
              .flowOn(Dispatchers.Default)
              .catch {
                  emitAll(flowOf("Minato", "Hashirama"))
              }
              .onCompletion {
                   println("Done")
                  it?.let {  throwable -> println(throwable) }//這裏由於上面處理了異常,所以這裏就不會再有異常傳遞,這裏自然也不會執行
              }
              .collect {
                  println(it)
              }
        }
    }
    //catch{}的作用就是捕獲異常並恢復新的Flow,這使得我們最終得到原始數據“Madara”, “Kakashi”和備份數據 “Minato”, “Hashirama”,捕獲異常之後就是一份全新的沒有異常的數據, onCompletion{…} and catch{…}都是mediator operators,他們時使用的順序很重要
    
  9. 總結

    我們使用Flow構建器創建了Flow,其中最基本的構建器是flowOf(),創建之後運行這個Flow需要使用terminal operators,由於terminal operator是suspend function ,因此我們需要在協程作用域內編寫Flow代碼,如果你不想使用這種嵌套調用而是鏈式調用,你可以使用 onEach{…}集合launchIn()。使用catch{}操作符來處理異常,並且當發生異常時也可以提供一個備份數據(如果你想這麼做)。當上遊的數據處理完或發生異常之後,使用onCompletion()來執行一些操作(感覺有點像finally)。。所有的操作符都會默認運行在調用函數的上下文中,可以使用flowOn()來切換上游的上下文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章