kotlin開發 Flow的學習

前言

  Flow是配合Kotlin協程使用的異步編程工具。其實Flow的概念並不是獨家的,更早之前Java端就有自帶的stream與大名鼎鼎的RxJava,它們的思想都是響應式編程思想(或者也可以稱呼鏈式編程),當時的響應式編程思想就是爲了解決Java各個線程的異步處理結果進行同步。其更底層的思想核心是觀察者模式或者生產消費者模式。所以回到Flow,它看似複雜有很多新概念,其實核心歸根結底就是觀察者模式思想與生產消費者模式思想。

Flow的使用結構

一個簡單的例子

一個極簡的例子快速瞭解一下,emit爲發射,collect爲接收

fun demo1() {
    GlobalScope.launch {
        flow<Int> {
            for (i in 0..10) {
                //emit爲發射函數
                emit(i)
            }
        }.collect {
            //collect接收,它還是掛起函數必須在協程中調用
            Log.e("zh", "結果 = ${it} ")
        }
    }
}

/*  輸出結果:
   結果 = 0
   結果 = 1
   結果 = 2
   結果 = 3
   結果 = 4
   結果 = 5
   結果 = 6
   結果 = 7
   結果 = 8
   結果 = 9
   結果 = 10
*/

構建方式

Flow有多種多樣的構建方式,下面一一舉例:

方式一 默認方式 

以默認的方式創建flow,上面的例子已經舉例了,這裏再舉例一下。

fun demo1() {
    GlobalScope.launch {
        flow<String> {
            delay(500)
            emit("蘋果")
            delay(500)
            emit("西瓜")
            delay(500)
            emit("香蕉")
            delay(500)
            emit("哈密瓜")
        }.collect{
            Log.e("zh", "時間 = ${System.currentTimeMillis()} 結果 = ${it}", )
        }
    }
}

/*  
    輸出結果:
    時間 = 1690598408614 結果 = 蘋果
    時間 = 1690598409116 結果 = 西瓜
    時間 = 1690598409617 結果 = 香蕉
    時間 = 1690598410118 結果 = 哈密瓜
 */

方式二 從集合發射流

通過集合調用asFlow()函數

fun demo2() {
    GlobalScope.launch {
        //從集合發射流
        (0..3).asFlow().collect {
            Log.e("zh", "結果 = ${it}", )
        }
        
        listOf<String>("蘋果","西瓜","香蕉","哈密瓜").asFlow().collect{
            Log.e("zh", "結果 = ${it}", )
        }
    }
}
/*
    輸出結果:
        結果 = 0
        結果 = 1
        結果 = 2
        結果 = 3
        結果 = 蘋果
        結果 = 西瓜
        結果 = 香蕉
        結果 = 哈密瓜
 */

方式三 自建集合發射流

通過調用flowOf()函數

fun demo2() {
    GlobalScope.launch {
        flowOf("蘋果" to 1, "香蕉" to 2, "西瓜" to 3).collect { pair ->
            Log.e("zh", "結果 = ${pair.first} ${pair.second}", )
        }
    }
}
/*
    輸出結果:
        結果 = 蘋果 1
        結果 = 香蕉 2
        結果 = 西瓜 3
 */

冷流

Flow是冷流的,冷流的概念是隻有訂閱者訂閱後,發佈者纔會生產數據。 並且訂閱者與發佈者是一一對應關係,數據只會給目標訂閱者發送,不會發送給其他。這類似於你去工廠下訂單,工廠纔會生產你需要的產品。

有冷流就會有熱流的概念,熱流其實是觀察者模式(或者廣播)的概念,發佈端無論有沒有訂閱者,都會始終執行,並且有多個訂閱者時,發佈端跟訂閱者是一對多的關係,熱流可以與多個訂閱者共享信息。

以下代碼驗證Flow的冷流特性:

fun demo3() {
    val flow = flow<String> {
        val simpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
        val timeString = simpleDateFormat.format(System.currentTimeMillis())
        emit(timeString)
    }

    GlobalScope.launch {
        for (index in 0..2){
            delay(1000)
            flow.collect {
                //collect調用它了,flow纔會執行
                Log.e("zh", "結果 = ${it} ")
            }
        }
    }
}

/*
    輸出結果:
        結果 = 13:53:53
        結果 = 13:53:54
        結果 = 13:53:55
 */

背壓概念 與 collectLatest 停止當前工作以收集最新值

Backpressure(背壓)

Backpressure(背壓)是指在異步場景中,被觀察者發送事件速度遠快於觀察者的處理速度的情況。在上面的例子中收集都使用了collect函數, collect會收集每一個值並且保證執行完處理代碼。 但是如果collect正在處理耗時數據,而flow的發送端是不會停下來,發送端會一直髮送,而collect則會堵塞發過來的數據,等待當前collect函數代碼塊裏的耗時邏輯處理完,纔會執行下一個發送過來的數據。這樣會導致我們看到最後最新的數據時已經延遲了很久。

collectLatest 停止當前工作以收集最新值

上面的背壓情況,在一些業務下並不希望耗時等待,只想儘快看到最後最新的數據,這個時候就可以使用collectLatest來替代collect進行數據收集。collectLatest並不會保證每一個發送來的時間都處理完,當有新數據發送過來時,如果collectLatest的代碼塊沒有執行完成,它會立刻停止處理老數據。去處理最新發送過來的數據。

代碼例子

fun demo1() {
    GlobalScope.launch {
        flow<Int> {
            for (i in 0..2){
                //請注意這裏延遲50毫秒是爲了對比下面collectLatest的延遲
                delay(50)
                emit(i)
            }
        }.collectLatest{
            //這裏分別延遲了2次50毫秒,是爲了表示這個代碼塊裏的耗時,是始終大於上面發送端的。
            delay(50)
            Log.e("zh", "模擬正在耗時處理 = ${it}")
            delay(50)
            Log.e("zh", "最終處理完結果 = ${it}")
        }
    }
}

/*
  輸出結果:
    模擬正在耗時處理 = 0
    模擬正在耗時處理 = 1
    模擬正在耗時處理 = 2
    最終處理完結果 = 2
 */

filter-過濾

filter屬於中間操作符,將數據過濾

fun demo3() {
    GlobalScope.launch {
        flow<Int> {
            for (i in 0..10){
                emit(i)
            }
        }.filter {
            //除餘,過濾掉奇數
            it % 2 == 0
        }.collect{
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        結果 = 0
        結果 = 2
        結果 = 4
        結果 = 6
        結果 = 8
        結果 = 10
 */

filterNot-過濾

filterNot屬於中間操作符, 它是反向過濾的,會將true的結果去除,將false結果繼續發射

fun demo3() {
    GlobalScope.launch {
        flow<Int> {
            for (i in 0..10){
                emit(i)
            }
        }.filterNot {
            //除餘,過濾掉偶數
            it % 2 == 0
        }.collect{
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        結果 = 1
        結果 = 3
        結果 = 5
        結果 = 7
        結果 = 9
 */

filterNotNull-過濾null值

filterNotNull屬於中間操作符,它會將null過濾掉。

fun demo1() {
    GlobalScope.launch {
        flow<String?> {
            //這裏的集合故意添加了一個null值
            listOf<String?>("蘋果", "香蕉", null, "芒果").forEach {
                Log.e("zh", "要發送的值 = ${it}")
                emit(it)
            }
        }.filterNotNull().collect {
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        要發送的值 = 蘋果
        結果 = 蘋果
        要發送的值 = 香蕉
        結果 = 香蕉
        要發送的值 = null
        要發送的值 = 芒果
        結果 = 芒果
 */

filterIsInstance-類型過濾

filterIsInstance屬於中間操作符,它會過濾數據類型

fun demo1() {
    GlobalScope.launch {
        flow {
            emit("蘋果")
            emit(1)
            emit(true)
            emit("芒果")
        }.filterIsInstance<String>() //這裏的filterIsInstance類型爲String,過濾除了String以外的類型
            .collect {
                Log.e("zh", "結果 = ${it}")
            }
    }
}

/*
    輸出結果:
        結果 = 蘋果
        結果 = 芒果
 */

map-轉換組合

map屬於中間操作符,用於將數據轉換或者組合

fun demo3() {
    val fruits = listOf<String>("蘋果", "香蕉", "芒果")
    GlobalScope.launch {
        flow<Int> {
            for (i in 0..2) {
                emit(i)
            }
        }.map { it: Int ->
            Pair<String, Int>(fruits[it], it)
        }.collect { it: Pair<String, Int> -> //這裏其實可以不用寫it: Pair<String, Int>,寫出來就是想表達,上面的flow<Int>被轉換成了Pair<String, Int>
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        結果 = (蘋果, 0)
        結果 = (香蕉, 1)
        結果 = (芒果, 2)
 */

mapLatest-轉換組合

mapLatest屬於中間操作符

有時候我們的map方法處理轉換需要耗時,但是發送端會一直髮射新數據,導致map的耗時堆積積累,這會出現獲取最後一個數據時已經耗時了很久。在一些情況下我們並不關切前面的數據,只希望獲取最新的數據,這裏就可以使用mapLatest。

fun demo3() {
    val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
    val flow = flow<Int> {
        for (i in 1..3){
            emit(i)
        }
    }
    GlobalScope.launch {
        //這裏實現map作爲下面mapLatest的對比
        flow.map {
            //這裏寫30毫秒延遲,模擬處理數據需要耗時
            delay(30)
            timeFormat.format(System.currentTimeMillis()) to it
        }.collect{
            Log.e("zh", "map >> 結果 = ${it}")
        }

        flow.mapLatest {
            //這裏寫30毫秒延遲,模擬處理數據需要耗時
            delay(30)
            timeFormat.format(System.currentTimeMillis()) to it
        }.collect{
            Log.e("zh", "mapLatest >> 最新數據結果 = ${it}")
        }
    }
}

/*
輸出結果:
    map >> 結果 = (15:25:03, 1)
    map >> 結果 = (15:25:03, 2)
    map >> 結果 = (15:25:03, 3)
    mapLatest >> 最新數據結果 = (15:25:03, 3)
 */

mapNotNull-轉換組合不包含null值

mapNotNull屬於中間操作符

fun demo1() {
    val fruits = listOf<String>("蘋果","芒果","水蜜桃","茄子")
    GlobalScope.launch {
        flow<String> {
            for (item in fruits){
                emit(item)
            }
        }.mapNotNull {
            if (it == "茄子"){
                //茄子不屬於水果,返回null
                null
            } else {
                System.currentTimeMillis() to it
            }
        }.collect{
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        結果 = (1690620115923, 蘋果)
        結果 = (1690620115923, 芒果)
        結果 = (1690620115923, 水蜜桃)
 */

drop-棄用指定數量前段數據

drop屬於中間操作符,它會棄用你給定數量的前段數據。

fun demo1() {
    GlobalScope.launch {
        flow {
            emit(1)
            emit(2)
            emit(3)
            emit(4)
        }.drop(2).collect{
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        結果 = 3
        結果 = 4
 */

dropWhile-找到首個不滿足條件的值,然後繼續發送它和剩下的數據

dropWhile屬於中間操作符,你不能簡單的將它理解成判斷範圍(這變成跟上面的過濾操作符一樣了),它其實用來找到數據集合的執行出發點。

fun demo1() {
    GlobalScope.launch {
        flow {
            emit(1)
            emit(2)
            emit(3)
            emit(4)
            emit(5)
            emit(6)
        }.dropWhile { it != 3 } //因爲dropWhile的特性,3這個值也會繼續發送
            .collect {
                Log.e("zh", "結果 = ${it}")
            }
    }
}

/*
    輸出結果:
        結果 = 3
        結果 = 4
        結果 = 5
        結果 = 6
 */

take-只允許指定數量的前段數據發送

take屬於中間操作符,它只允許指定數量的前段數據發送

fun demo1() {
    GlobalScope.launch {
        flow {
            emit(1)
            emit(2)
            emit(3)
            emit(4)
            emit(5)
            emit(6)
        }.take(3).collect {
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        結果 = 1
        結果 = 2
        結果 = 3
 */

takeWhile-找到首個不滿足條件的值,發送它前面的值

takeWhile屬於中間操作符

fun demo1() {
    GlobalScope.launch {
        flow {
            emit(1)
            emit(2)
            emit(3)
            emit(4)
            emit(5)
            emit(6)
        }.takeWhile { it != 3 }.collect {
            Log.e("zh", "結果 = ${it}")
        }
    }
}

/*
    輸出結果:
        結果 = 1
        結果 = 2
 */

debounce-防抖動,指定接受數據的超時時間

debounce屬於中間操作符,請注意,debounce這個函數有點複雜。它的意思是指定超時時間,如果發送端邏輯耗時大於超時時間就不發送(但是如果一直都超時就會發送最後一個數據),只有發送端邏輯耗時小於超時時間才發送。

 

定時接收

 

切換線程

 

異常捕獲

 

end

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