Kotlin 學(xué)習(xí)筆記(五)—— Flow 數(shù)據(jù)流學(xué)習(xí)實(shí)踐指北(一)

Kotlin 學(xué)習(xí)筆記艱難地來(lái)到了第五篇~ 在這一篇主要會(huì)說(shuō) Flow 的基本知識(shí)和實(shí)例焕蹄。由于 Flow 內(nèi)容較多,所以會(huì)分幾個(gè)小節(jié)來(lái)講解蕾久,這是第一小節(jié),文章后面會(huì)結(jié)合一個(gè)實(shí)例介紹 Flow 在實(shí)際開(kāi)發(fā)中的應(yīng)用新娜。

首先回想一下,在協(xié)程中處理某個(gè)操作既绩,我們只能返回單個(gè)結(jié)果概龄;而 Flow 可以按順序返回多個(gè)結(jié)果,在官方文檔中饲握,F(xiàn)low 被翻譯為 數(shù)據(jù)流私杜,這也說(shuō)明了 Flow 適用于多值返回的場(chǎng)景。

Flow 是以協(xié)程為基礎(chǔ)構(gòu)建的互拾,所以它可通過(guò)異步的方式處理一組數(shù)據(jù)歪今,所要處理的數(shù)據(jù)類(lèi)型必須相同,比如:Flow<Int>是處理整型數(shù)據(jù)的數(shù)據(jù)流颜矿。

Flow 一般包含三個(gè)部分:
1)提供方:負(fù)責(zé)生成數(shù)據(jù)并添加到 Flow 中寄猩,得益于協(xié)程,F(xiàn)low 可以異步生成數(shù)據(jù)骑疆;
2)中介(可選):可對(duì) Flow 中的值進(jìn)行操作田篇、修改;也可修改 Flow 本身的一些屬性箍铭,如所在線程等泊柬;
3)使用方:接收并使用 Flow 中的值。
提供方:生產(chǎn)者诈火,使用方:消費(fèi)者兽赁,典型的生產(chǎn)者消費(fèi)者模式。

1. Flow 概述

Flow 是一個(gè)異步數(shù)據(jù)流冷守,它可以順序地發(fā)出數(shù)據(jù)刀崖,通過(guò)流上的一些中間操作得出結(jié)果;若出錯(cuò)可拋出異常拍摇。這些 “流上的中間操作” 包括但不限于 map亮钦、filtertake充活、zip 等等方法蜂莉。這些中間操作是鏈?zhǔn)降睦ⅲ梢栽诤竺嬖俅翁砑悠渌僮鞣椒ǎ⑶乙膊皇菕炱鸷瘮?shù)映穗,它們只是構(gòu)建了一條鏈?zhǔn)降牟僮鞑?shí)時(shí)返回結(jié)果給后面的操作步驟窖张。

流上的終端操作符要么是掛起函數(shù),例如 collect男公、single荤堪、toList 等等合陵,要么是在給定作用域內(nèi)開(kāi)始收集流的 launchIn 操作符枢赔。前半句好理解,后半句啥意思拥知?這就得看一下 launchIn 這個(gè)終端操作符的作用了踏拜。它里面是這樣的:

//code 1
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
    collect() // tail-call
}

原來(lái) launchIn 方法可以傳入一個(gè) CoroutineScope 協(xié)程作用域,然后在這個(gè)作用域里面調(diào)用 collect 方法低剔。lifecycleScope速梗、MainScope() 這些都是協(xié)程作用域,所以 launchIn 方法只不過(guò)是 scope.launch { flow.collect() } 的一種簡(jiǎn)寫(xiě)襟齿。

流的執(zhí)行也被稱(chēng)之為收集流姻锁,并且是以掛起的方式,不是阻塞的猜欺。流最終的執(zhí)行成功與否取決于流上的操作是否全部執(zhí)行成功位隶。collect 函數(shù)就是最常見(jiàn)的收集流函數(shù)。

1.1 冷流與熱流

冷流(Cold Flow):在數(shù)據(jù)被使用方訂閱后开皿,即調(diào)用 collect 方法之后涧黄,提供方才開(kāi)始執(zhí)行發(fā)送數(shù)據(jù)流的代碼,通常是調(diào)用 emit 方法赋荆。即不消費(fèi)笋妥,不生產(chǎn),多次消費(fèi)才會(huì)多次生產(chǎn)窄潭。使用方和提供方是一對(duì)一的關(guān)系春宣。

熱流(Hot Flow):無(wú)論有無(wú)使用方,提供方都可以執(zhí)行發(fā)送數(shù)據(jù)流的操作嫉你,提供方和使用方是一對(duì)多的關(guān)系月帝。熱流就是不管有無(wú)消費(fèi),都可生產(chǎn)均抽。

SharedFlow 就是熱流的一種嫁赏,任何流也可以通過(guò) stateInshareIn 操作轉(zhuǎn)化為熱流,或者通過(guò) produceIn 操作將流轉(zhuǎn)化為一個(gè)熱通道也能達(dá)到目的油挥。本篇只介紹冷流相關(guān)知識(shí)潦蝇,熱流會(huì)在后面小節(jié)講解~

2. Flow 構(gòu)建方法

Flow 的構(gòu)造方法有如下幾種:
1款熬、 flowOf() 方法。用于快速創(chuàng)建流攘乒,類(lèi)似于 listOf() 方法贤牛,下面是它的源碼:

//code 2
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
    for (element in elements) {
        emit(element)
    }
}

所以用法也比較簡(jiǎn)單:

//code 3
val testFlow = flowOf(65,66,67)
lifecycleScope.launch {
    testFlow.collect {
        println("輸出:$it")
    }
}
//打印結(jié)果:
//輸出:65
//輸出:66
//輸出:67

注意到 Flow 初始化的時(shí)候跟其他對(duì)象一樣,作用域在哪兒都可以则酝,但 collect 收集的時(shí)候就需要放在協(xié)程里了殉簸,因?yàn)?collect 是個(gè)掛起函數(shù)。

2沽讹、asFlow() 方法般卑。是集合的擴(kuò)展方法,可將其他數(shù)據(jù)轉(zhuǎn)換成 Flow爽雄,例如 Array 的擴(kuò)展方法:

//code 4
public fun <T> Array<T>.asFlow(): Flow<T> = flow {
    forEach { value ->
        emit(value)
    }
}

不僅 Array 擴(kuò)展了此方法蝠检,各種其他數(shù)據(jù)類(lèi)型的數(shù)組都擴(kuò)展了此方法。所以集合可以很方便地構(gòu)造一個(gè) Flow挚瘟。

3叹谁、flow {···} 方法。這個(gè)方法可以在其內(nèi)部順序調(diào)用 emit 方法或 emitAll 方法從而構(gòu)造一個(gè)順序執(zhí)行的 Flow乘盖。emit 是發(fā)射單個(gè)值焰檩;emitAll 是發(fā)射一個(gè)流,這兩個(gè)方法分別類(lèi)似于 list.add(item)订框、list.addAll(list2) 方法析苫。flow {···} 方法的源碼如下:

//code 5
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)

需要額外注意的是,flow 后面的 lambda 表達(dá)式是一個(gè)掛起函數(shù)布蔗,里面不能使用不同的 CoroutineContext 來(lái)調(diào)用 emit 方法去發(fā)射值藤违。因此,在 flow{...} 中不要通過(guò)創(chuàng)建新協(xié)程或使用 withContext 代碼塊在另外的 CoroutineContext 中調(diào)用 emit 方法纵揍,否則會(huì)報(bào)錯(cuò)顿乒。如果確實(shí)有這種需求,可以使用 channelFlow 操作符泽谨。

//code 6
val testFlow = flow {
    emit(23)
//    withContext(Dispatchers.Main) { // error
//        emit(24)
//    }
    delay(3000)
    emitAll(flowOf(25,26))
}

4璧榄、channelFlow {···} 方法。這個(gè)方法就可以在內(nèi)部使用不同的 CoroutineContext 來(lái)調(diào)用 send 方法去發(fā)射值吧雹,而且這種構(gòu)造方法保證了線程安全也保證了上下文的一致性骨杂,源碼如下:

//code 7
public fun <T> channelFlow(@BuilderInference block: suspend ProducerScope<T>.() -> Unit): Flow<T> =
    ChannelFlowBuilder(block)

一個(gè)簡(jiǎn)單的使用例子:

//code 8
val testFlow1 = channelFlow {
    send(20)
    withContext(Dispatchers.IO) { //可切換線程
        send(22)
    }
}
lifecycleScope.launch {
    testFlow1.collect {
        println("輸出 = $it")
    }
}

5、MutableStateFlowMutableSharedFlow 方法:都可以定義相應(yīng)的構(gòu)造函數(shù)去創(chuàng)建一個(gè)可以直接更新的熱流雄卷。由于篇幅有限搓蚪,有關(guān)熱流的知識(shí)后面小節(jié)會(huì)再說(shuō)明。

3. Flow 常用的操作符

Flow 的使用依賴(lài)于眾多的操作符丁鹉,這些操作符可以大致地分為 中間操作符末端操作符 兩大類(lèi)妒潭。中間操作符是流上的中間操作悴能,可以針對(duì)流上的數(shù)據(jù)做一些修改,是鏈?zhǔn)秸{(diào)用雳灾。中間操作符與末端操作符的區(qū)別是:中間操作符是用來(lái)執(zhí)行一些操作漠酿,不會(huì)立即執(zhí)行,返回值還是個(gè) Flow谎亩;末端操作符就會(huì)觸發(fā)流的執(zhí)行炒嘲,返回值不是 Flow。

一個(gè)完整的 Flow 是由 Flow 構(gòu)建器匈庭、Flow 中間操作符夫凸、Flow 末端操作符 組成,如下示意圖所示:

圖1 Flow 的組成

3.1 collect 末端操作符

最常見(jiàn)的當(dāng)然是 collect 操作符嚎花。它是個(gè)掛起函數(shù)寸痢,需要在協(xié)程作用域中調(diào)用;并且它是一個(gè)末端操作符紊选,末端操作符就是實(shí)際啟動(dòng) Flow 執(zhí)行的操作符,這一點(diǎn)跟 RxJava 中的 Observable 對(duì)象的執(zhí)行很像道逗。

熟悉 RxJava 的同學(xué)知道兵罢,在 RxJava 中,Observable 對(duì)象的執(zhí)行開(kāi)始時(shí)機(jī)是在被一個(gè)訂閱者(subscriber) 訂閱(subscribe) 的時(shí)候滓窍,即在 subscribe 方法調(diào)用之前卖词,Observable 對(duì)象的主體是不會(huì)執(zhí)行的。

Flow 也是相同的工作原理吏夯,F(xiàn)low 在調(diào)用 collect 操作符收集流之前此蜈,F(xiàn)low 構(gòu)建器和中間操作符都不會(huì)執(zhí)行。舉個(gè)栗子:

//code 9
val testFlow2 = flow {
    println("++++ 開(kāi)始")
    emit(40)
    println("++++ 發(fā)出了40")
    emit(50)
    println("++++ 發(fā)出了50")
}
lifecycleScope.launch {
    testFlow2.collect{
        println("++++ 收集 = $it")
    }
}

// 輸出結(jié)果:
//com.example.myapplication I/System.out: ++++ 開(kāi)始
//com.example.myapplication I/System.out: ++++ 收集 = 40
//com.example.myapplication I/System.out: ++++ 發(fā)出了40
//com.example.myapplication I/System.out: ++++ 收集 = 50
//com.example.myapplication I/System.out: ++++ 發(fā)出了50

從輸出結(jié)果可以看出噪生,每次到 collect 方法調(diào)用時(shí)裆赵,才會(huì)去執(zhí)行 emit 方法,而在此之前跺嗽,emit 方法是不會(huì)被調(diào)用的战授。這種 Flow 就是冷流。

3.2 reduce 末端操作符

reduce 也是一個(gè)末端操作符桨嫁,它的作用就是將 Flow 中的數(shù)據(jù)兩兩組合接連進(jìn)行處理植兰,跟 Kotlin 集合中的 reduce 操作符作用相同。舉個(gè)栗子:

//code 10
private fun reduceOperator() {
    val testFlow = listOf("w","i","f","i").asFlow()
    CoroutineScope(Dispatchers.Default).launch {
        val result = testFlow.reduce { accumulator, value ->
            println("+++accumulator = $accumulator  value = $value")
            "$accumulator$value"
        }
        println("+++final result = $result")
    }
}

//輸出結(jié)果:
//com.example.myapplication I/System.out: +++accumulator = w  value = i
//com.example.myapplication I/System.out: +++accumulator = wi  value = f
//com.example.myapplication I/System.out: +++accumulator = wif  value = i
//com.example.myapplication I/System.out: +++final result = wifi

看結(jié)果就知道璃吧,reduce 操作符的處理邏輯了楣导,兩個(gè)值處理后得到的新值作為下一輪中的輸入值之一,這就是兩兩接連進(jìn)行處理的意思畜挨。

圖1 中出現(xiàn)的 toList 操作符也是一種末端操作符筒繁,可以將 Flow 返回的多個(gè)值放進(jìn)一個(gè) List 中返回彬坏,返回的 List 也可以自己設(shè)置,比較簡(jiǎn)單膝晾,感興趣的同學(xué)可自行動(dòng)手試驗(yàn)栓始。

3.3 zip 中間操作符

zip 顧名思義,就是可以將兩個(gè) Flow 匯合成一個(gè) Flow血当,舉個(gè)栗子就知道了:

//code 11
lateinit var testFlow1: Flow<String>
lateinit var testFlow2: Flow<String>
private fun setupTwoFlow() {
    testFlow1 = flowOf("Red", "Blue", "Green")
    testFlow2 = flowOf("fish", "sky", "tree", "ball")
    CoroutineScope(Dispatchers.IO).launch {
        testFlow1.zip(testFlow2) { firstWord, secondWord ->
            "$firstWord $secondWord"
        }.collect {
            println("+++ $it +++")
        }
    }
}

// 輸出結(jié)果:
//com.example.myapplication I/System.out: +++ Red fish +++
//com.example.myapplication I/System.out: +++ Blue sky +++
//com.example.myapplication I/System.out: +++ Green tree +++

//zip 方法聲明:
public fun <T1, T2, R> Flow<T1>.zip(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R> = zipImpl(this, other, transform)

zip 方法的聲明中可知幻赚,zip 方法的第二個(gè)參數(shù)就是針對(duì)兩個(gè) Flow 進(jìn)行各種處理的掛起函數(shù),也可如例子中寫(xiě)成尾調(diào)函數(shù)的樣子臊旭,返回值是處理之后的 Flow落恼。而且當(dāng)兩個(gè) Flow 長(zhǎng)度不一樣時(shí),最后的結(jié)果會(huì)默認(rèn)剔除掉先前較長(zhǎng)的 Flow 中的元素离熏。所以 testFlow2 中的 “ball” 就被自動(dòng)剔除掉了佳谦。

4. Flow 異常處理

正如 RxJava 框架中的 subscribe 方法可以通過(guò)傳入 Observer 對(duì)象在其 onNextonComplete滋戳、onError 返回之前處理的結(jié)果钻蔑,F(xiàn)low 也有諸如 catchonCompletion 等操作符去處理執(zhí)行的結(jié)果奸鸯。例如下面的代碼:

//code 12
private fun handleExceptionDemo() {
    val testFlow = (1..5).asFlow()
    CoroutineScope(Dispatchers.Default).launch {
        testFlow.map {
            check(it != 3) {
                //it == 3 時(shí)咪笑,會(huì)走到這里
                println("+++ catch value = $it")
            }
            println("+++ not catch value = $it")
            it * it
        }.onCompletion {
            println("+++ onCompletion value = $it")
        }.catch { exception ->
            println("+++ catch exception = $exception")
        }.collect{
            println("+++ collect value = $it")
        }
    }
}

//輸出結(jié)果:
//com.example.myapplication I/System.out: +++ not catch value = 1
//com.example.myapplication I/System.out: +++ collect value = 1
//com.example.myapplication I/System.out: +++ not catch value = 2
//com.example.myapplication I/System.out: +++ collect value = 4
//com.example.myapplication I/System.out: +++ catch value = 3
//com.example.myapplication I/System.out: +++ onCompletion value = java.lang.IllegalStateException: kotlin.Unit
//com.example.myapplication I/System.out: +++ catch exception = java.lang.IllegalStateException: kotlin.Unit

順著代碼咱先來(lái)看看一些常用的 Flow 中間操作符。
1)map :用來(lái)將 Flow 中的數(shù)據(jù)一個(gè)個(gè)拿出來(lái)做各自的處理娄涩,然后交給下一個(gè)操作符窗怒;本例中就是將 Flow 中的數(shù)據(jù)進(jìn)行平方處理;
2)check() :類(lèi)似于一個(gè)檢查站蓄拣,滿足括號(hào)內(nèi)條件的數(shù)據(jù)可以通過(guò)扬虚,不滿足則交給它的尾調(diào)函數(shù)處理,并且拋出異常球恤;
3)onCompletion :Flow 最后的兜底器辜昵。無(wú)論 Flow 最后是執(zhí)行完成、被取消碎捺、拋出異常路鹰,都會(huì)走到 onCompletion 操作符中,類(lèi)似于在 Flow 的 collect 函數(shù)外加了個(gè) try收厨,finally晋柱。官方給了個(gè)小栗子,還是很清楚的:

//code 13
try {
    myFlow.collect { value ->
        println(value)
    }
} finally {
    println("Done")
}
//上述代碼可以替換為下面的代碼:
myFlow
    .onEach { println(it) }
    .onCompletion { println("Done") }
    .collect()

所以诵叁,在 code 12 中的 onCompletion 操作符可以接住從 check 那兒拋出的異常雁竞;
4)catch :不用多說(shuō),專(zhuān)門(mén)用于捕捉異常的,避免程序崩潰碑诉。這里如果把 catch 去掉彪腔,程序就會(huì)崩潰。如果把 catchonCompletion 操作符位置調(diào)換进栽,則 onCompletion 里面就接收不到異常信息了德挣,如圖所示。

圖2 catch 操作符

5. Flow 數(shù)據(jù)請(qǐng)求實(shí)例

說(shuō)了這么多快毛,舉個(gè)在實(shí)際中經(jīng)常用到的數(shù)據(jù)請(qǐng)求的例子吧格嗅。先來(lái)看一個(gè)最簡(jiǎn)單的例子:

5.1 單接口請(qǐng)求

現(xiàn)在一般都是在 ViewModel 里持有 LiveData 數(shù)據(jù),并且進(jìn)行數(shù)據(jù)的請(qǐng)求唠帝,所以先來(lái)看下 ViewModel 中的代碼實(shí)現(xiàn):

//code 14
class SingleNetworkCallViewModel: ViewModel() {
    private val users = MutableLiveData<Resource<List<ApiUser>>>()
    private val apiHelperImpl = ApiHelperImpl(RetrofitBuilder.apiService)

    fun fetchUsers() {
        viewModelScope.launch {
            users.postValue(Resource.loading(null))
            apiHelperImpl.getUsers()
                .catch { e ->
                    users.postValue(Resource.error(e.toString(), null))
                }
                .collect {
                    users.postValue(Resource.success(it))
                }
        }
    }

    fun getUsersData(): LiveData<Resource<List<ApiUser>>> {
        return users
    }
}

從代碼可看出屯掖,fetchUsers 方法就是數(shù)據(jù)請(qǐng)求方法,里面的核心方法是 ApiHelperImpl 類(lèi)對(duì)象的 getUsers 方法襟衰,在之前初始化 apiHelperImpl 對(duì)象時(shí)傳入了一個(gè) RetrofitBuilder.apiService 值贴铜,所以底層還是用到了 Retrofit 框架進(jìn)行的網(wǎng)絡(luò)請(qǐng)求。Retrofit 相關(guān)的代碼如下:

//code 15
object RetrofitBuilder {
    private const val BASE_URL = "https://xxxxxxx/"

    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}

//ApiService 中的代碼也是一般常見(jiàn)的代碼:
interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<ApiUser>
}

再回過(guò)來(lái)看看 ViewModel 的代碼瀑晒,從 apiHelperImpl.getUsers 方法后面的 catchcollect 操作符也可看出惰说,getUsers 方法返回的就是一個(gè) Flow 對(duì)象赚瘦,其使用的構(gòu)造方法就是前文中說(shuō)到的 flow{} 方法:

//code 16
class ApiHelperImpl(private val apiService: ApiService) : ApiHelper {
    override fun getUsers(): Flow<List<ApiUser>> {
        return flow { emit(apiService.getUsers()) }
    }
}

ApiHelper 其實(shí)就是一個(gè)接口破停,規(guī)定了 ApiHelperImpl 中數(shù)據(jù)請(qǐng)求的方法名及返回值骇扇,返回值是一個(gè) Flow,里面是我們最終需要的數(shù)據(jù)列表:

//code 17
interface ApiHelper {
    fun getUsers(): Flow<List<ApiUser>>
}

Flow 調(diào)用 emit 發(fā)出去的就是 Retrofit 進(jìn)行數(shù)據(jù)請(qǐng)求后返回的 List<ApiUser> 數(shù)據(jù)间坐。

如何在 Activity 中使用就是之前使用 LiveData 的常規(guī)操作了:

//code 18
private fun setupObserver() {
    viewModel.getUsersData().observe(this, Observer {
        when (it.status) {
            Status.SUCCESS -> {
                progressBar.visibility = View.GONE
                it.data?.let { users -> renderList(users) }
                recyclerView.visibility = View.VISIBLE
            }
            Status.LOADING -> {
                progressBar.visibility = View.VISIBLE
                recyclerView.visibility = View.GONE
            }
            Status.ERROR -> {
                //Handle Error
                progressBar.visibility = View.GONE
                Toast.makeText(this, it.message, Toast.LENGTH_SHORT).show()
            }
        }
    })
}

5.2 雙接口并行請(qǐng)求

上述例子是最簡(jiǎn)單的單個(gè)數(shù)據(jù)接口請(qǐng)求的場(chǎng)景,如果是兩個(gè)或是多個(gè)數(shù)據(jù)接口需要并行請(qǐng)求邑退,該如何處理呢竹宋?這就需要用到之前說(shuō)的 Flow 中的 zip 操作符了。接著上面的例子地技,再添加一個(gè)數(shù)據(jù)請(qǐng)求方法 getMoreUsers 蜈七,那么兩個(gè)接口并行的例子為:

//code 18
fun fetchUsers() {
    viewModelScope.launch {
        users.postValue(Resource.loading(null))
        apiHelper.getUsers()
            .zip(apiHelper.getMoreUsers()) { usersFromApi, moreUsersFromApi ->
                val allUsersFromApi = mutableListOf<ApiUser>()
                allUsersFromApi.addAll(usersFromApi)
                allUsersFromApi.addAll(moreUsersFromApi)
                return@zip allUsersFromApi
            }
            .flowOn(Dispatchers.Default)
            .catch { e ->
                users.postValue(Resource.error(e.toString(), null))
            }
            .collect {
                users.postValue(Resource.success(it))
            }
    }
}

兩個(gè)數(shù)據(jù)接口請(qǐng)求的快慢肯定不一樣,但不用擔(dān)心莫矗,zip 操作符會(huì)等待兩個(gè)接口的數(shù)據(jù)都返回之后才進(jìn)行拼接并交給后面的操作符處理飒硅,所以這里還需要調(diào)用 flowOn 操作符將線程切換到后臺(tái)線程中去掛起等待。但后面的 collect 操作符執(zhí)行的代碼是在主線程中作谚,感興趣的同學(xué)可以打印線程信息看看三娩,這就需要了解一下 flowOn 操作符的用法了。

flowOn 方法可以切換 Flow 處理數(shù)據(jù)的所在線程妹懒,類(lèi)似于 RxJava 中的 subscribeOn 方法雀监。例如 flowOn(Dispatchers.Default) 就是將 Flow 的操作都放到后臺(tái)線程中執(zhí)行。

當(dāng) flowOn 操作符之前沒(méi)有設(shè)置任何的協(xié)程上下文,那么 flowOn 操作符可以為它之前的操作符設(shè)置執(zhí)行所在的線程会前,并不會(huì)影響它之后下游的執(zhí)行所在線程好乐。下面是一個(gè)簡(jiǎn)單例子:

//code 19
private fun flowOnDemo() {
    val testFlow = (1..2).asFlow()
    MainScope().launch {
        testFlow
            .filter {
                println("1+++ $it  ${Thread.currentThread().name}")
                it != 3
            }.flowOn(Dispatchers.IO)
            .map {
                println("2+++ $it  ${Thread.currentThread().name}")
                it*it
            }.flowOn(Dispatchers.Main)
            .filter {
                println("3+++ $it  ${Thread.currentThread().name}")
                it!=25
            }.flowOn(Dispatchers.IO)
            .collect{
                println("4+++ $it  ${Thread.currentThread().name}")
            }
    }
}

//輸出結(jié)果:
//com.example.myapplication I/System.out: 1+++ 1  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 1+++ 2  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 2+++ 1  main
//com.example.myapplication I/System.out: 2+++ 2  main
//com.example.myapplication I/System.out: 3+++ 1  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 3+++ 4  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 4+++ 1  main
//com.example.myapplication I/System.out: 4+++ 4  main

發(fā)現(xiàn)了么?flowOn 操作符只對(duì)最近的上游操作符線程負(fù)責(zé)瓦宜,它下游的線程會(huì)自動(dòng)切換到之前所在的線程蔚万。如果連續(xù)有兩個(gè)或多個(gè) flowOn 操作符切換線程,則會(huì)切換到首個(gè) flowOn 操作符切換的線程中去:

//code 20
testFlow
    .filter {
        println("1+++ $it  ${Thread.currentThread().name}")
        it != 3    //最終會(huì)在 Main 主線程中執(zhí)行
    }.flowOn(Dispatchers.Main).flowOn(Dispatchers.IO).flowOn(Dispatchers.Default)
    .collect{
        println("4+++ $it  ${Thread.currentThread().name}")
}

filter 后面連續(xù)有兩個(gè) flowOn 操作符临庇,但最終會(huì)在 Main 線程中執(zhí)行 filter 操作符中的邏輯反璃。

整體上看,F(xiàn)low 在數(shù)據(jù)請(qǐng)求時(shí)所扮演的角色是數(shù)據(jù)接收與處理后發(fā)送給 UI 層的作用苔巨,這跟 RxJava 的職責(zé)是相同的版扩,而且兩者都有豐富的操作符來(lái)處理各種不同的情況。不同的是 Flow 是將接收到的數(shù)據(jù)放到 Flow 載體中侄泽,而 RxJava 一般將數(shù)據(jù)放到 Observable 對(duì)象中礁芦;Flow 處理數(shù)據(jù)更加方便和自然,去除了 RxJava 中繁多且功能臃腫的操作符悼尾。

總結(jié)

最后總結(jié)一下 Flow 第一小節(jié)的內(nèi)容吧:
1)Flow 數(shù)據(jù)流可異步按順序返回多個(gè)數(shù)據(jù)柿扣;
2)Flow 整體是由 構(gòu)建器中間操作符闺魏、末端操作符 組成未状;
3)冷流只有在調(diào)用末端操作符時(shí),流的構(gòu)造器和中間操作符才會(huì)開(kāi)始執(zhí)行析桥;冷流的使用方和提供方是一對(duì)一的司草;
4)簡(jiǎn)單介紹了 collectreduce 末端操作符以及 zip泡仗、map 等中間操作符的使用埋虹;
5)Flow 異常處理所用到的 catchcheck娩怎、onCompletion 等操作符的用法搔课;
6)Flow 在數(shù)據(jù)請(qǐng)求上的實(shí)例
所用實(shí)例來(lái)源:https://github.com/MindorksOpenSource/Kotlin-Flow-Android-Examples

更多內(nèi)容,歡迎關(guān)注公眾號(hào):修之竹

贊人玫瑰截亦,手留余香爬泥!歡迎點(diǎn)贊、轉(zhuǎn)發(fā)~ 轉(zhuǎn)發(fā)請(qǐng)注明出處~

參考文獻(xiàn)

  1. Android 上的 Kotlin 數(shù)據(jù)流崩瓤;官方文檔 https://developer.android.com/kotlin/flow
  2. Flow Kotlin 官方文檔袍啡; https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
  3. 【Kotlin Flow】 一眼看全——Flow操作符大全; 搬磚小子出現(xiàn)了 https://juejin.cn/post/6989536876096913439
  4. What is Flow in Kotlin and how to use it in Android Project?; Himanshu Singh; https://blog.mindorks.com/what-is-flow-in-kotlin-and-how-to-use-it-in-android-project
  5. Understanding Terminal Operators in Kotlin Flow; Amit Shekhar; https://blog.mindorks.com/terminal-operators-in-kotlin-flow
  6. Creating Flow Using Flow Builder in Kotlin; Amit Shekhar; https://blog.mindorks.com/creating-flow-using-flow-builder-in-kotlin
  7. Exception Handling in Kotlin Flow; Amit Shekhar; https://blog.mindorks.com/exception-handling-in-kotlin-flow
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谷遂,一起剝皮案震驚了整個(gè)濱河市葬馋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖畴嘶,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛋逾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窗悯,警方通過(guò)查閱死者的電腦和手機(jī)区匣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蒋院,“玉大人亏钩,你說(shuō)我怎么就攤上這事∑劬桑” “怎么了姑丑?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辞友。 經(jīng)常有香客問(wèn)我栅哀,道長(zhǎng),這世上最難降的妖魔是什么称龙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任留拾,我火速辦了婚禮,結(jié)果婚禮上鲫尊,老公的妹妹穿的比我還像新娘痴柔。我一直安慰自己,他們只是感情好疫向,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布咳蔚。 她就那樣靜靜地躺著,像睡著了一般搔驼。 火紅的嫁衣襯著肌膚如雪屹篓。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天匙奴,我揣著相機(jī)與錄音,去河邊找鬼妄荔。 笑死泼菌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的啦租。 我是一名探鬼主播哗伯,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼篷角!你這毒婦竟也來(lái)了焊刹?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虐块,沒(méi)想到半個(gè)月后俩滥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贺奠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年霜旧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片儡率。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挂据,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出儿普,到底是詐尸還是另有隱情崎逃,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布眉孩,位于F島的核電站个绍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏勺像。R本人自食惡果不足惜障贸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吟宦。 院中可真熱鬧篮洁,春花似錦、人聲如沸殃姓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蜗侈。三九已至篷牌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踏幻,已是汗流浹背枷颊。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留该面,地道東北人夭苗。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像隔缀,于是被迫代替她去往敵國(guó)和親题造。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容