kotlin Flow 學習指南 (三)最終篇

目錄

  • 前言
  • Flow生命周期
  • StateFlow 替代LiveData
  • SharedFlow
  • 其他常見應用場景
    • 處理復雜、耗時邏輯
    • 存在依賴關系的接口請求
    • 組合多個接口的數據
  • Flow使用注意事項
  • 總結

前言

前面兩篇文章,介紹了Flow是什么,如何使用,以及相關的操作符進階,接下來這篇文章,主要介紹Flow在實際項目中使用。

Flow生命周期

在介紹Flow實際應用場景之前,我們先回顧Flow第一篇介紹的計時器例子,我們在ViewModel定義了一個timeFlow數據流:

class MainViewModel : ViewModel() {val timeFlow = flow {var time = 0while (true) {emit(time)delay(1000)time++}
}

然后Activity里面,接收前面定義的數據流。

lifecycleOwner.lifecycleScope.launch {viewModel.timeFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}

我運行看下實際效果:

flow1.gif

你們有沒有發現,App切換到后臺時,日志還在打印,這不是對資源的浪費,我們修改一下接收的地方代碼:

lifecycleOwner.lifecycleScope.launchWhenStarted {viewModel.timeFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}

我們把協程開啟的方法,從launch改成launchWhenStarted,再運行看下效果:

flow2.gif

我們可以看到,當點擊HOME鍵,退回到后臺的時候,日志不再打印了,由此可見,改動生效了,但是流取消接收了嗎,我們切回到前臺看下:

flow3.gif

切換到前臺,我們可以看到,計數器并沒有從0開始,所以其實它并沒有取消接收,只是在后臺暫停接收數據了,Flow管道還保留之前的數據,事實上這個launchWhenStarted API已經廢棄了,Google更推薦repeatOnLifecycle來代替它,并且它不會存在管道中保留舊數據問題。
我們嘗試改造一下對應代碼:

lifecycleOwner.lifecycleScope.launch {lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.timeFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}
}

重新運行看下效果:

flow4.gif

我們可以看到,從后臺切回到前臺數據又從0開始了,說明切換到后臺,Flow取消工作了,原來的數據全部清空了。

我們在使用Flow,通過repeatOnLifecycle,更能保證我們程序的安全性。

StateFlow 替代LiveData

前面介紹的都是Flow冷流例子,接下來將會介紹一些熱流常見的應用場景。
還是前面的計時器的例子,假如橫豎屏切換后,又會出現什么情況呢?

flow5.gif

我們可以看到,橫豎屏切換后,Activity重新創建,重新創建后,timeFlow會重新collect,冷流被重新collect后重新執行,然后計時器又從0開始計時了,很多時候,我們希望橫豎屏切換時,希望頁面的狀態是保持不變的,至少在一定時間內不被改變的,這里我們冷流修改成熱流試下:

val hotFlow =timeFlow.stateIn(viewModelScope,SharingStarted.WhileSubscribed(5000),0)```
lifecycleOwner.lifecycleScope.launch {lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.hotFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}
}
```

這里著重說下stateIn里面的三個參數,第一個是協程的作用域,第二個是flow保持工作狀態最大有效時間,超過flow就會停止工作,最后一個參數是初始值。

重新運行看下效果:

flow6.gif

這里我們可以看到橫豎屏切換后,打印的日志,計時器不會從0開始了。
我們上面介紹了一個冷流如何修改變成熱流的,這里還沒有介紹stateFlow如何代替LiveData,下面介紹一下,stateFlow替代LiveData用法:

private val _stateFlow = MutableStateFlow(0)
val stateFlow = _stateFlow.asStateFlow()fun startTimer() {val timer = Timer()timer.scheduleAtFixedRate(object :TimerTask() {override fun run() {_stateFlow.value += 1}},0,1000)
}```viewModel.startTimer()lifecycleOwner.lifecycleScope.launch {lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.stateFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}
}
```

我們定義了一個StateFlow熱流,然后通過一個startTimer()方法改變stateFlow值類似LiveData setData,點擊按鈕時,開始改變StateFlow值并收集對應流的值類似LiveData Observe方法監聽數據變化。
下面看下實際運行效果:

flow7.gif

到這里,我們介紹完了StateFlow基本用法,下面來介紹SharedFlow。

SharedFlow

要理解SharedFlow,我們先知道個概念,粘性事件,按字面理解就是,觀察者訂閱數據源時,如果數據源已經有最新的數據,那么這些數據會立即推送給觀察者。從上面的解釋來看,LiveData是符合這個粘性特性的,同樣的StateFlow呢?我們寫個簡單的demo驗證一下:


class MainViewModel : ViewModel() {private val _clickCountFlow = MutableStateFlow(0)val clickCountFlow = _clickCountFlow.asStateFlow()fun increaseClickCount() {_clickCountFlow.value += 1
}
}
//MainActivity
```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {viewModel.increaseClickCount()
}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.clickCountFlow.collect { time ->tv.text = time.toString()Log.d("ddup", "update UI $time")}}
}
```

我們首先在MainViewModel,定義了一個clickCountFlow,然后在Activity,通過Button點擊對clickCountFlow數據改變,然后接收clickCountFlow并把數據顯示在文本上。
下面看下運行效果:

flow8.gif

我們可以看到橫豎屏切換的時候,Activity重新創建,clickCountFlow重新收集后,數據還是從之前的4開始的,說明StateFlow是粘性的,在這里看上去沒有問題,但是我們看另外一個例子,我們模擬一個點擊登陸的場景,點擊登陸按鈕,實現登陸并登陸:

//MainViewModelprivate val _loginFlow = MutableStateFlow("")val loginFlow = _loginFlow.asStateFlow()fun startLogin() {// Handle login logic here._loginFlow.value = "Login Success"}
//MainActivity```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {viewModel.startLogin()
}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.loginFlow.collect {if (it.isNotBlank()) {Toast.makeText(this@MainActivity2, it, Toast.LENGTH_LONG).show()}}}
}
```

上述代碼實際就是模擬一個點擊登陸,然后會提示登陸成功,我們看下實際運行效果:

flow9.gif

看到沒有,橫豎屏切換后,登陸成功的提示重新彈出一遍,我們并沒有走重新登陸流程,這就是粘性事件帶來的數據重復接收的問題,上面代碼,我們改成SharedFlow試下:

    private val _loginFlow = MutableSharedFlow<String>()val loginFlow = _loginFlow.asSharedFlow()fun startLogin() {// Handle login logic here.viewModelScope.launch {_loginFlow.emit("Login Success")}}

我們StateFlow改成SharedFlow,我們可以看到SharedFlow不需要初始值,登陸的地方增加了emit方法發送數據,接收數據的地方不變,重新運行下看下效果:

flow10.gif

這里我們可以看到使用SharedFlow不會出現這個粘性問題,其實SharedFlow還有很多參數可以配置的:

    public fun <T> MutableSharedFlow(// 每個新的訂閱者訂閱時收到的回放的數目,默認0replay: Int = 0,// 除了replay數目之外,緩存的容量,默認0extraBufferCapacity: Int = 0,// 緩存區溢出時的策略,默認為掛起。只有當至少有一個訂閱者時,onBufferOverflow才會生效。當無訂閱者時,只有最近replay數目的值會保存,并且onBufferOverflow無效。onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND)

SharedFlow更多用法,有待大家去發掘啊,這里不過贅述了。

其他常見應用場景

前面介紹了從基本冷流到熱流,以及StateFlow、SharedFlow常見用法,適用場景,接下來,我們圍繞幾個實際例子,看看flow其他常見應用場景。

處理復雜、耗時邏輯

我們一般做一些復雜的耗時邏輯,放在子線程處理,然后切換到主線程展示UI,同樣的Flow也支持線程切換,flowOn可以讓之前的操作放到對應的子線程處理。
我們實現一個讀取本地Assets目錄下的person.json文件,并將其解析出來,json文件中的內容:

{"name": "ddup","age": 101,"interest": "earn money..."
}

然后解析文件:

fun getAssetJsonInfo(context: Context, fileName: String): String {val strBuilder = StringBuilder()var input: InputStream? = nullvar inputReader: InputStreamReader? = nullvar reader: BufferedReader? = nulltry {input = context.assets.open(fileName, AssetManager.ACCESS_BUFFER)inputReader = InputStreamReader(input, StandardCharsets.UTF_8)reader = BufferedReader(inputReader)var line: String?while ((reader.readLine().also { line = it }) != null) {strBuilder.append(line)}} catch (ex: Exception) {ex.printStackTrace()} finally {try {input?.close()inputReader?.close()reader?.close()} catch (e: IOException) {e.printStackTrace()}}return strBuilder.toString()
}

Flow讀取文件:

/*** 通過Flow方式,獲取本地文件*/
private fun getFileInfo() {lifecycleScope.launch {flow {//解析本地json文件,并生成對應字符串val configStr = getAssetJsonInfo(this@MainActivity2, "person.json")//最后將得到的實體類發送到下游emit(configStr)}.map { json ->Gson().fromJson(json, PersonModel::class.java) //通過Gson將字符串轉為實體類}.flowOn(Dispatchers.IO) //在flowOn之上的所有操作都是在IO線程中進行的.onStart { Log.d("ddup", "onStart") }.filterNotNull().onCompletion { Log.d("ddup", "onCompletion") }.catch { ex -> Log.d("ddup", "catch:${ex.message}") }.collect {Log.d("ddup", "collect parse result:$it")}}
}

最終打印日志:

2024-07-09 22:00:34.006 12251-12251 ddup com.ddup.flowtest D onStart 2024-07-09 22:00:34.018 12251-12251 ddup com.ddup.flowtest D collect parse result:PersonModel(name=ddup, age=101, interest=earn money...) 2024-07-09 22:00:34.019 12251-12251 ddup com.ddup.flowtest D onCompletion

存在依賴關系的接口請求

我們經常會遇到接口請求依賴另外一個請求的結果,也就是所謂的嵌套請求,嵌套過多的就會出現回調地獄,我們通過FLow來實現一個類似的需求:

lifecycleScope.launch {lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {//將兩個flow串聯起來 先搜索目的地,然后到達目的地viewModel.getTokenFlows().flatMapConcat {//第二個flow依賴第一個的結果viewModel.getUserFlows(it)}.collect {tv.text = it ?: "error"}}
}

組合多個接口的數據

組合多個接口的數據是一個什么樣的場景呢,比如說,我們存在請求多個接口,然后把它們的結果合并起來統一展示或者作為另外一個接口的請求參數,試問一下,該如何實現呢:
第一種,一個一個請求,然后合并;
第二種,并發請求,然后全部請求完了合并。
顯然,第二種效果比較高效,下面看下代碼:

//分別請求電費、水費、網費,Flow之間是并行關系
suspend fun requestElectricCost(): Flow<SpendModel> =flow {delay(500)emit(SpendModel("電費", 10f, 500))}.flowOn(Dispatchers.IO)suspend fun requestWaterCost(): Flow<SpendModel> =flow {delay(1000)emit(SpendModel("水費", 20f, 1000))}.flowOn(Dispatchers.IO)suspend fun requestInternetCost(): Flow<SpendModel> =flow {delay(2000)emit(SpendModel("網費", 30f, 2000))}.flowOn(Dispatchers.IO)

首先,我們在ViewModel模擬定義了,幾個網絡請求,接下來合并請求:

lifecycleScope.launch {val electricFlow = viewModel.requestElectricCost()val waterFlow = viewModel.requestWaterCost()val internetFlow = viewModel.requestInternetCost()val builder = StringBuilder()var totalCost = 0fval startTime = System.currentTimeMillis()//NOTE:注意這里可以多個zip操作符來合并Flow,且多個Flow之間是并行關系electricFlow.zip(waterFlow) { electric, water ->totalCost = electric.cost + water.costbuilder.append("${electric.info()},\n").append("${water.info()},\n")}.zip(internetFlow) { two, internet ->totalCost += internet.costtwo.append(internet.info()).append(",\n\n總花費:$totalCost")}.collect {tv.text = it.append(",總耗時:${System.currentTimeMillis() - startTime} ms")Log.d("ddup","${it.append(",總耗時:${System.currentTimeMillis() - startTime} ms")}")}
}

運行結果:
flow11.png
我們看到總花費時間,跟最長請求的時間基本一致。

Flow使用注意事項

多個Flow不能放到一個lifecycleScope.launch里去collect{},因為進入collect{}相當于一個死循環,下一行代碼永遠不會執行;如果就想寫到一個lifecycleScope.launch{}里去,可以在內部再開啟launch{}子協程去執行。
錯誤示范:

lifecycleScope.launch {flow1.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}flow2.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}
}

正確寫法:

lifecycleScope.launch {launch {flow1.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}}launch {flow2.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}}
}

總結

我們從Flow的生命周期,介紹了flow正確使用姿勢,避免資源的浪費,到普通的冷流轉換成熱流,再到StateFlow代替LiveData,以及它的粘性問題,然后通過SharedFlow解決粘性問題,再到常見應用場景,最后到Flow使用注意事項,基本涵蓋了Flow大部分特性、應用場景,這也是Flow學習的最終篇。
創作不易,喜歡的麻煩點贊、收藏、評論,以資鼓勵
參考文章
Kotlin Flow響應式編程,StateFlow和SharedFlow

Kotlin | Flow數據流的幾種使用場景

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/43392.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/43392.shtml
英文地址,請注明出處:http://en.pswp.cn/web/43392.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

如何挑選適合的需求池管理系統?10款優質工具分享

本文將分享10款優質需求池管理工具&#xff1a;PingCode、Worktile、Teambition、Epicor Kinetic、TAPD、SAP IBP、Logility、RELEX Solutions、JIRA、明道云。 在管理項目和產品需求時&#xff0c;正確的工具能夠大幅提高效率與透明度。如何從眾多需求池工具中選擇最適合團隊的…

第一節 SHELL腳本中的常用命令(2)

二,網絡管理命令nmcli 1.查看網卡 # 或者先用ip addr或ip a等查看網卡 ip a s 網卡名 ifconfig 網卡名 nmcil device show 網卡名 nmcil device status nmcil connection show 網卡名2.設置網卡 a)當網卡未被設置過時 設置dncp網絡工作模式 nmcil connection add con-name…

Rust編程-編寫自動化測試

編寫單元測試步驟&#xff1a; 1. 準備所需的數據 2. 調用需要測試的代碼 3. 斷言運行結果與我們所期望的一致 Rust的test元數據&#xff1a; #[cfg(test)]&#xff1a;是一個屬性宏&#xff08;attribute macro&#xff09;。用于控制特定的代碼段僅在測試環境中編譯…

自定義類型:聯合體

像結構體一樣&#xff0c;聯合體也是由一個或者多個成員組成&#xff0c;這些成員可以是不同的類型。 聯合體類型的聲明 編譯器只為最?的成員分配?夠的內存空間。聯合體的特點是所有成員共?同?塊內存空間。所以聯合體也叫&#xff1a;共?體。 輸出結果&#xff1a; 聯合體…

size_t 數據類型的好處

什么是size_t size_t 類型在不同的平臺上對應不同的底層整數類型&#xff0c;具體取決于平臺的指針大小。size_t 主要用于表示大小和長度&#xff0c;如數組的元素數量、緩沖區的大小等&#xff0c;它的設計目的是為了匹配指針的大小&#xff0c;以避免類型不匹配引起的錯誤。…

代碼隨想錄算法訓練營DAY58|101.孤島的總面積、102.沉沒孤島、103. 水流問題、104.建造最大島嶼

忙。。。寫了好久。。。。慢慢補吧。 101.孤島的總面積 先把周邊的島嶼變成水dfs def dfs(x, y, graph, s):if x<0 or x>len(graph) or y<0 or y>len(graph[0]) or graph[x][y]0:return sgraph[x][y]0s1s dfs(x1, y, graph, s)s dfs(x-1, y, graph, s)s dfs(…

【爬蟲入門知識講解:xpath】

3.3、xpath xpath在Python的爬蟲學習中&#xff0c;起著舉足輕重的地位&#xff0c;對比正則表達式 re兩者可以完成同樣的工作&#xff0c;實現的功能也差不多&#xff0c;但xpath明顯比re具有優勢&#xff0c;在網頁分析上使re退居二線。 xpath 全稱為XML Path Language 一種…

軟考高級第四版備考--第16天(規劃溝通管理)Plan Communication Management

定義&#xff1a;基于每個干系人或干系人群體的信息需求、可用的組織資產以及具體的項目的需求&#xff0c;為項目溝通活動制定恰當的方法和計劃的過程。 作用&#xff1a; 及時向干系人提供相關信息&#xff1b;引導干系人有效參與項目&#xff1b;編制書面溝通計劃&#xf…

【基于R語言群體遺傳學】-16-中性檢驗Tajima‘s D及連鎖不平衡 linkage disequilibrium (LD)

Tajimas D Test 已經開發了幾種中性檢驗&#xff0c;用于識別模型假設的潛在偏差。在這里&#xff0c;我們將說明一種有影響力的中性檢驗&#xff0c;即Tajimas D&#xff08;Tajima 1989&#xff09;。Tajimas D通過比較數據集中的兩個&#x1d703; 4N&#x1d707;估計值來…

vue項目中常見的一些preset及其關系

Babel的作用 Babel主要用途是用來做js代碼轉換的&#xff0c;將最新的js語法或者api轉換成低版本瀏覽器可兼容執行的代碼。 語法兼容是指一些瀏覽器新特性增加的js寫法&#xff0c;例如箭頭函數 ()>{}&#xff1b;低版本的瀏覽器無法識別這些&#xff0c;會導致一些語法解…

spark shuffle寫操作——UnsafeShuffleWriter

PackedRecordPointer 使用long類型packedRecordPointer存儲數據。 數據結構為&#xff1a;[24 bit partition number][13 bit memory page number][27 bit offset in page] LongArray LongArray不同于java中long數組。LongArray可以使用堆內內存也可以使用堆外內存。 Memor…

秋招突擊——7/9——字節面經

文章目錄 引言正文八股MySQL熟悉嗎&#xff1f;講一下MySQL索引的結構&#xff1f;追問&#xff1a;MySQL為什么要使用B樹&#xff1f;在使用MySQL的時候&#xff0c;如何避免索引失效&#xff1f;講一下MySQL的事物有哪幾種特征&#xff1f;MySQL的原子性可以實現什么效果&…

GESP C++ 三級真題(2023年9月)T2 進制判斷

進制判斷 問題描述 N進制數指的是逢N進一的計數制。例如&#xff0c;人們日常生活中大多使用十進制計數&#xff0c; 而計算機底層則一般使用二進制。除此之外&#xff0c;八進制和十六進制在一些場合也是 常用的計數制(十六進制中&#xff0c;一般使用字母A至F表示十至十五…

【區塊鏈+跨境服務】粵澳健康碼跨境互認系統 | FISCO BCOS應用案例

2020 年突如其來的新冠肺炎疫情&#xff0c;讓社會治理體系面臨前所未見的考驗&#xff0c;如何兼顧疫情防控與復工復產成為社會 各界共同努力的目標。區塊鏈技術作為傳遞信任的新一代信息基礎設施&#xff0c;善于在多方協同的場景中發揮所長&#xff0c;從 而為粵澳兩地的疫情…

uniapp上傳文件并獲取上傳進度

1. 上傳普通文件 uni.chooseMessageFile({count: 1,success: (res) > {console.log(res)console.log("res123456", res.tempFiles[0].path)const uploadTask uni.uploadFile({url: http://localhost:8000/demo,filePath: res.tempFiles[0].path,name: file,form…

CSS關于居中的問題

文章目錄 1. 行內和塊級元素自身相對父控件居中1.1. 塊級元素相對父控件居中1.2. 行內元素相對于父控件居中 2. 實現單行文字垂直居中3. 子絕父相實現子元素的水平垂直居中3.1. 方案一3.1.1. 示例 3.2. 方案二3.2.1. 示例 3.3. 方案三(推薦)3.3.1. 示例 3.4. 方案四(了解一下) …

AI大模型知識點大梳理_ai大模型的精度以下哪項描述的準確

AI大模型是什么 AI大模型是指具有巨大參數量的深度學習模型&#xff0c;通常**包含數十億甚至數萬億個參數。**這些模型可以通過學習大量的數據來提高預測能力&#xff0c;從而在自然語言處理、計算機視覺、自主駕駛等領域取得重要突破。 AI大模型的定義具體可以根據參數規模…

短信驗證碼研究:公開的短信驗證碼接口、不需要注冊的短信驗證碼接口

短信驗證碼研究&#xff1a;公開的短信驗證碼接口、不需要注冊的短信驗證碼接口 0 說明 本文提供了一個短信驗證碼接口&#xff0c;主要用于以下場景&#xff1a; 1、用于開發調試 2、用于申請驗證碼困難的企業和個人 3、用于短信驗證碼認證還沒有通過&#xff0c;但是著急…

DBeaver操作MySQL無法同時執行多條語句的解決方法

DBeaver選擇數據庫連接&#xff0c;在【驅動屬性】中將allowMultiQueries允許執行多條語句置為True

各種音頻處理器

在HiFi&#xff08;高保真&#xff09;音頻系統中&#xff0c;通常需要使用一些特定類型的音頻處理器&#xff0c;以確保音頻信號的高保真和優質輸出。以下是一些常見的音頻處理器類型及其在HiFi系統中的應用&#xff1a; DAC&#xff08;數模轉換器&#xff09;&#xff1a; …