彻底搞清Flow+MVVM+Retrofit+OKHTTP框架

一、概述

本文主要介绍了Flow的基本使用方法,结合了Flow+MVVM+Retrofit+OKHTTP框架,能够带你理解FlowViewModelRepository,切换线程等方面的细节 全是本人在学习过程中疑问的解答!

二、对 Flow 的基本认识

Flow 翻译过来就是"流"的意思,与其说是流我个人感觉用"管道 "来比喻更加恰当。而对于这个"管道"我们需要往里面去放入 数据,并且在恰当的时候取出 数据。在不使用流的情况下,我们一般会采用 livedataRxJava 等一系列框架来实现响应式系统 ,什么意思呢,就是我们可以很自然的去发觉数据的变化 ,因为包括网络请求数据库读取 等一系列 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里面写的代码和协程一样,也应该有自己的作用域 。而且flowRetrofit,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是什么?官方推荐使用这两种方法:

  1. 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,相互不影响。

  1. 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..."
)

先来看一下冷热流的概念:

  1. 冷流:

数据生产者与消费者绑定 ,数据只有在有消费者(collect)时才开始生产 。每个消费者都会从头开始接收数据。

  1. 热流:

数据生产者独立 于消费者,热流会在创建后立即开始 生产数据,数据的生产和消费是独立 的。新加入的消费者只能接收到数据流的最新数据

介绍完冷热流的概念,我们再来看一下冷热流的转换

shareInstateIn分别将其转换为 SharedFlowStateFlow,这两个流都属于是热流

那为什么要在 ViewModel 中使用 SharedFlow 呢?他有没好处吗?

ViewModel 的设计目的是在配置更改(如屏幕旋转)后保留数据,而 StateFlow 天生适合存储最新状态。那转换为热流了,数据什么时候发射的?难道就像上面热流的性质一样主动触发吗?

注意一下stateIn中传入的数据,SharingStarted.WhileSubscribed(5000) 表示:当有活跃订阅者 时开始收集上游数据,当最后一个订阅者取消后,等待 5 秒再停止上游流。好吧,虽然是热流但怎么还有冷流的特点啊。

其他的行为:

  • SharingStarted.Eagerly: 立即启动上游流,无视订阅者是否存在。
  • SharingStarted.Lazily:在第一个订阅者出现时启动,永久保持活跃。

再看一下另一个传入的参数 ,上面写的是Loading...,因为热流他需要个默认值,所以一定要先给他传一个,但是这个值写Loading...肯定是不会的,这也会导致 Flowcollcect的时候不知道数据是什么类型的只能返回一个 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>()
}

再加个异常处理,flowcatch用法和普通的其实也差不多,这里就不细说了。

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 哦!

谢谢你们的点赞和和关注啦!

相关推荐
renxhui34 分钟前
Android 性能优化(四):卡顿优化
android·性能优化
二流小码农1 小时前
鸿蒙开发:UI界面分析利器ArkUI Inspector
android·ios·harmonyos
CYRUS_STUDIO1 小时前
FART 精准脱壳:通过配置文件控制脱壳节奏与范围
android·安全·逆向
小疯仔1 小时前
使用el-input数字校验,输入汉字之后校验取消不掉
android·开发语言·javascript
墨狂之逸才2 小时前
Data Binding Conversion 详解
android
iceBin2 小时前
uniapp打包安卓App热更新,及提示下载安装
android·前端
杨充2 小时前
高性能图片优化方案
android
墨狂之逸才3 小时前
BindingAdapter名称的对应关系、命名规则和参数定义原理
android
hellokai3 小时前
ReactNative介绍及简化版原理实现
android·react native
阿豪元代码3 小时前
Perfetto 上手指南3 —— CPU 信息分析
android