一、概述
本文主要介绍了Flow
的基本使用方法,结合了Flow+MVVM+Retrofit+OKHTTP
框架,能够带你理解Flow
在ViewModel
,Repository
,切换线程等方面的细节 。 全是本人在学习过程中疑问的解答!
二、对 Flow 的基本认识
Flow
翻译过来就是"流"的意思,与其说是流我个人感觉用"管道 "来比喻更加恰当。而对于这个"管道"我们需要往里面去放入 数据,并且在恰当的时候取出 数据。在不使用流的情况下,我们一般会采用 livedata
,RxJava
等一系列框架来实现响应式系统 ,什么意思呢,就是我们可以很自然的去发觉数据的变化 ,因为包括网络请求 ,数据库读取 等一系列 IO 操作都是在非主线程进行的,而 UI 的更新必须在主线程,Flow
就是基于协程的另一种实现方式。
三、发射数据
使用 flow 按照上面说的,那总需要一个管道吧?新建管道的方法有这么三种。
scss
val Flow1 = flow {
emit("内容")
}
scss
val Flow2 = flowOf(1, 2)
val Flow3 = listOf(3, 4).asFlow()
看一下emit
它居然是个挂起函数!
kotlin
public suspend fun emit(value: T)
往管道中发射数据需要用到emit
函数,都说 Flow
对协程的支持非常好用,我觉得主要体现在流中能直接调用发射 这个一挂起函数。
kotlin
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T>
点进去看看flow
方法,看到了个 suspend 使用能调用挂起函数 ,那这就说明了flow
里面写的代码和协程一样,也应该有自己的作用域 。而且flow
对 Retrofit
,Room
等都有很好的支持。
看一下我下面这个与Retrofit 结合使用的例子(MVVM 框架):
kotlin
//service
@GET("user/isLogin")
suspend fun isLogin(): BaseResponse<String>
//Repository
fun isLogin(): Flow<BaseResponse<String>> = flow{
emit(service.isLogin())
}
新建一个管道,直接emit
一个挂起函数就行了,并且暴露出这么一个方法,这个方法的return
就是包含了网络请求返回值的管道。我们在接下里的操作中直接调用这个方法的就可以了。这里就有个问题了,网络请求是在哪个线程中执行的呢?这个问题我们先留着。
四、处理数据
管道中数据在流的时候,可以直接对其进行修改和处理。有以下那么多种方法可供选择。
1.map 修改
scss
val myFlow = flowOf(1, 2)
// 将数据做 +2 处理
myFlow.map {
it + 2
}
类似于这种方法,处理完之后依然是个 Flow,并且类型不会发生改变。
2.filter 筛选
ini
flowOf(1, 2, 3).filter { it % 2 == 0 }
3.transform 变换
上面两个函数 lambda 表达式中不能 emit,而在这个函数的 lambda 表达式中必须要 emit
,而且可以 emit
多个值。
scss
val flow= flowOf(1, 2, 3).transform { value ->
emit(value * 2)
emit(value * 3)
}
4.take 获取
最多获取几个数据
scss
flowOf(1, 2, 3, 4).take(2)
5.combine 合并
两个管道可以合并成一个,合并的逻辑是对当前持有数据的合并。
scss
// 模拟两个实时更新的数据流
val userInputFlow = flowOf("A", "B", "C").onEach { delay(500) } // 每500ms发射一个字母
val counterFlow = (1..3).asFlow().onEach { delay(1000) } // 每1秒发射一个数字
// 使用 combine 合并流
userInputFlow.combine(counterFlow) { input, count ->
"组合结果:Input=$input, Count=$count"
}
// 结果
// 1A
// 2A
// 3A
// 3B
// 3C
五、收集数据
最常用的方法肯定是collect
啦。对任意一个管道调用collect
,当每一个数据到达的时候都会执行一次collect
中的代码。要注意的是collect
是个挂起函数,需要在挂起函数中调用。
scss
flowOf(1, 2, 3).collect { println(it) }
另外还有两种不常用的。
scss
val list = flowOf(1, 2, 3).toList()
scss
val first = flowOf(1, 2, 3).first()
回到上面的网络请求,我们在 emit
中调用了一个挂起函数,当这个挂起函数返回之后,emit
就会发射出一个数据,那我们要怎么样在 activity
/fragment
中收集这个数据呢?
javascript
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.ifLogin.collect{
//更新ui
Log.d("MainActivity!", it.data.toString())
}
}
}
我们可以直接在lifecycleScope
所在的协程中更新 ui,因为它启动的协程是在主线程上的。
repeatOnLifecycle
是什么?官方推荐使用这两种方法:
- repeatOnLifecycle()
是个挂起函数,我们需要通过lifecycleScope.launch
先启动一个协程再调用。
kotlin
public suspend fun LifecycleOwner.repeatOnLifecycle(
state: Lifecycle.State,
block: suspend CoroutineScope.() -> Unit
): Unit = lifecycle.repeatOnLifecycle(state, block)
state
:在指定的生命周期状态(如 STARTED
/RESUMED
)进入时启动协程块,并在状态退出时自动取消协程。本质上就是一个挂起函数,直接控制协程的启动和取消。
代码块内的所有操作(如 collect
)仅在生命周期活跃时执行。并且,可包裹多个数据流的收集逻辑。
调用repeatOnLifecycle
的协程 将不会继续执行后面的代码了,当它恢复的时候,已经是DESTROY
的时候了。我们是不能拿这个特性做点事情呢?
所以不要在repeatOnLifecycle
的后面继续repeatOnLifecycle
。官方推荐在repeatOnLifecycle
里面launch
多次,开启多个协程,然后在里面collect
,相互不影响。
- flowWithLifecycle()
less
lifecycleScope.launch {
viewModel.ifLogin.flowWithLifecycle(lifecycle).collect{
Log.d("MainActivity!", it.data.toString())
Log.d("MainActivity!",Thread.currentThread().name)
}
}
kotlin
@OptIn(ExperimentalCoroutinesApi::class)
public fun <T> Flow<T>.flowWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T> = callbackFlow {
lifecycle.repeatOnLifecycle(minActiveState) {
[email protected] {
send(it)
}
}
close()
}
如果只有一个Flow
数据需要收集,那么官方推荐使用flowWithLifecycle
,它其实就是将原始 Flow
转换为一个新的 Flow
,该流仅在生命周期达到或超过 指定状态(看上面代码,有默认值STARTED
)时发射数据,否则停止发射。本质是通过包装原始流实现生命周期感知。
外面要调用 lifecycleScope.launch
就是因为它本身就是个流,调用流的collect
要在协程中。
六、Flow 的上下文问题
回到上面留下的关于线程的问题,如果在Repository 中这么写:
kotlin
fun isLogin(): Flow<BaseResponse<String>> = flow{
emit(service.isLogin())
}
flow 流就会默认在主线程中执行,也就是说我们在主线程中执行了 Io 操作,这是极度危险的行为,为了解决这个问题,很容易的就能想到 flow 本质是个协程,在里面用 withContext 切换一下上下文,不就能实现切换线程的操作了吗。恭喜你喜提一段报错:
报错说要用 flowOn
来管理上下文问题,接下来我们就来介绍一下它吧。每个 flowOn
仅作用于其上游操作符(简单来说就是上一对括号中的代码会切换),形成分阶段的线程切换:
scss
// 模拟网络请求
fun fetchUserFlow(userId: String): Flow<User> = flow {
val user = apiService.getUser(userId) // IO 线程执行
emit(user)
}.map { user ->
user.copy(name = user.name.uppercase()) // IO 线程执行
}.flowOn(Dispatchers.IO)
.map { user ->
calculateStatistics(user) // Default 线程执行
}.flowOn(Dispatchers.Default)
.onEach { user ->
saveToDatabase(user) // IO 线程执行
}.flowOn(Dispatchers.IO)
// 收集端
viewModelScope.launch {
fetchUserFlow("123").collect { user ->
updateUI(user) // 主线程执行
}
}
那这样呢?
kotlin
fun isLogin(): Flow<BaseResponse<String>> = flow {
Log.d("MyRepository", Thread.currentThread().name)
emit(service.isLogin())
}.flowOn(Dispatchers.IO).flowOn(Dispatchers.Main)
分段原理,只与上一段有关,flowOn(Dispatchers.IO)
这对于它上面这一段,flowOn(Dispatchers.Main)
也针对于它上面那一段,但是它上一段没用任何操作,所以flowOn(Dispatchers.Main)
这一段等于无效。
加与不加完全是一样的。接下来看这种情况:
kotlin
fun isLogin(): Flow<BaseResponse<String>> = flow {
Log.d("MyRepository", Thread.currentThread().name)
emit(service.isLogin())
}.flowOn(Dispatchers.IO).map {
Log.d("MyRepository", Thread.currentThread().name)
it
}
对于 map
没有 flowon
作用于它,所以他是默认的线程:
scss
flowOf(1, 2, 3).conflate()
conflate() 在 Kotlin Flow 中的作用是处理背压(Backpressure)问题,其核心行为是:当收集器(Collector)处理当前值时,若上游又发射了新值,则丢弃中间的所有值,直接保留并传递最新的值。这种机制适用于只需处理最新数据、中间状态可忽略的场景。
七、集成 ViewModel
ViewModel 持有什么呢?本质上它应该持有数据,但是我们现在用的是 flow
,难道要在 ViewModel 里面collect
吗?那显然是不对的,因为你要在修改 ui
的时候collect
啊!那就只有一种可能性了,直接持有 flow
!就像这样:
ini
val ifLogin = repository.isLogin()
然后在 activity 种这杨写:
javascript
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.ifLogin.collect{
Log.d("MainActivity!", it.data.toString())
}
}
}
仔细思考一下,又不对啊,他是个流啊!你想要collect
了它再去生产数据,最后被你收集到,这样ViewModel
的意义不就没了吗?
官方给了一种写法:
ini
val ifLogin = repository.isLogin().stateIn(
viewModelScope, SharingStarted.WhileSubscribed(5000), "Loading..."
)
先来看一下冷热流的概念:
- 冷流:
数据生产者与消费者绑定 ,数据只有在有消费者(collect)时才开始生产 。每个消费者都会从头开始接收数据。
- 热流:
数据生产者独立 于消费者,热流会在创建后立即开始 生产数据,数据的生产和消费是独立 的。新加入的消费者只能接收到数据流的最新数据。
介绍完冷热流的概念,我们再来看一下冷热流的转换。
shareIn
或stateIn
分别将其转换为 SharedFlow
和 StateFlow
,这两个流都属于是热流。
那为什么要在 ViewModel 中使用 SharedFlow
呢?他有没好处吗?
ViewModel 的设计目的是在配置更改(如屏幕旋转)后保留数据,而 StateFlow
天生适合存储最新状态。那转换为热流了,数据什么时候发射的?难道就像上面热流的性质一样主动触发吗?
注意一下stateIn
中传入的数据,SharingStarted.WhileSubscribed(5000)
表示:当有活跃订阅者 时开始收集上游数据,当最后一个订阅者取消后,等待 5 秒再停止上游流。好吧,虽然是热流但怎么还有冷流的特点啊。
其他的行为:
SharingStarted.Eagerly
: 立即启动上游流,无视订阅者是否存在。SharingStarted.Lazily
:在第一个订阅者出现时启动,永久保持活跃。
再看一下另一个传入的参数 ,上面写的是Loading...
,因为热流他需要个默认值,所以一定要先给他传一个,但是这个值写Loading...
肯定是不会的,这也会导致 Flow
在 collcect
的时候不知道数据是什么类型的只能返回一个 Any
类型的,造成极大的不便!
所以肯定不能这也写!那初始值到底要设置为什么才合理呢?我这边想了个办法。包装一个状态类型BaseResponseState
专门用来处理状态,泛型 T 只需要传入你要请求返回的数据接收类型就可以了。
kotlin
sealed class BaseResponseState<T> {
// 成功状态,携带数据
data class Success<T>(val data: T) : BaseResponseState<T>()
// 失败状态,携带错误信息
data class Error<T>(val message: String, val code: Int = -1) : BaseResponseState<T>()
// 加载中状态
class Loading<T> : BaseResponseState<T>()
}
再加个异常处理,flow
的catch
用法和普通的其实也差不多,这里就不细说了。
kotlin
//Repository
fun isLogin(): Flow<BaseResponseState<BaseResponse<String>>> = flow {
emit(BaseResponseState.Loading())
// 发起网络请求
val isLogin = service.isLogin()
// 发射成功状态
emit(BaseResponseState.Success(isLogin))
}.flowOn(Dispatchers.IO).catch {
// 发射失败状态
emit(BaseResponseState.Error(it.message ?: "未知的网络请求失败"))
}
//ViewModel
val ifLogin = repository.isLogin().stateIn(
viewModelScope, SharingStarted.WhileSubscribed(5000),
BaseResponseState.Loading()
)
//activity
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.ifLogin.collect {
when (it) {
is BaseResponseState.Loading -> {
Log.d("MainActivity!", "Loading")
}
is BaseResponseState.Success -> {
Log.d("MainActivity!", it.data.message.toString())
}
is BaseResponseState.Error -> {
Log.d("MainActivity!", it.message)
}
}
}
}
}
只有在新值与当前值不相等 (通过 equals 方法判断)时,才会触发 collcect。先看一下输出情况:
- 请求正常的时候:
- 请求错误的时候:
注意看两个 Loading,一个是热流的默认值,一个是刚刚开始自己 emit
的。逻辑是:如果新值 与旧值 相同(如未正确更新对象),collect
不会收到通知,反之观察到数据变化就会触发 collect
。
到此为止我们就彻底将 Flow
+ MVVM
+ Retrofit
+ OKHTTP
搞清楚啦!
八、结尾
欢迎光临我的 GitHub:CoreCodeLibrary
里面能找到完整的代码!
喜欢的话记得给 star 哦!
谢谢你们的点赞和和关注啦!