目錄
- 前言
- 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")}}
我運行看下實際效果:
你們有沒有發現,App切換到后臺時,日志還在打印,這不是對資源的浪費,我們修改一下接收的地方代碼:
lifecycleOwner.lifecycleScope.launchWhenStarted {viewModel.timeFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}
我們把協程開啟的方法,從launch改成launchWhenStarted,再運行看下效果:
我們可以看到,當點擊HOME鍵,退回到后臺的時候,日志不再打印了,由此可見,改動生效了,但是流取消接收了嗎,我們切回到前臺看下:
切換到前臺,我們可以看到,計數器并沒有從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")}}
}
重新運行看下效果:
我們可以看到,從后臺切回到前臺數據又從0開始了,說明切換到后臺,Flow取消工作了,原來的數據全部清空了。
我們在使用Flow,通過repeatOnLifecycle,更能保證我們程序的安全性。
StateFlow 替代LiveData
前面介紹的都是Flow冷流例子,接下來將會介紹一些熱流常見的應用場景。
還是前面的計時器的例子,假如橫豎屏切換后,又會出現什么情況呢?
我們可以看到,橫豎屏切換后,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就會停止工作,最后一個參數是初始值。
重新運行看下效果:
這里我們可以看到橫豎屏切換后,打印的日志,計時器不會從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方法監聽數據變化。
下面看下實際運行效果:
到這里,我們介紹完了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并把數據顯示在文本上。
下面看下運行效果:
我們可以看到橫豎屏切換的時候,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()}}}
}
```
上述代碼實際就是模擬一個點擊登陸,然后會提示登陸成功,我們看下實際運行效果:
看到沒有,橫豎屏切換后,登陸成功的提示重新彈出一遍,我們并沒有走重新登陸流程,這就是粘性事件帶來的數據重復接收的問題,上面代碼,我們改成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方法發送數據,接收數據的地方不變,重新運行下看下效果:
這里我們可以看到使用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")}")}
}
運行結果:
我們看到總花費時間,跟最長請求的時間基本一致。
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數據流的幾種使用場景