彻底搞清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 哦!

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

相关推荐
雾里看山9 分钟前
【MySQL】用户管理和权限
android·mysql·adb
_祝你今天愉快25 分钟前
Android源码学习之Overlay
android·源码
顾林海27 分钟前
Flutter Dart 异常处理全面解析
android·前端·flutter
獨枭1 小时前
Mac 上 Android Studio 的安装与配置指南
android·macos·android studio
rainboy2 小时前
对Parcelable/Serializable的一点理解
android·java·源码
顾林海3 小时前
Android线程与线程池:高效编程的基石
android
QING6184 小时前
一文带你吃透CopyWriteArrayList的内部实现
android·java·数据结构
QING6184 小时前
一文带你吃透ConcurrentHashMap的实现和使用
android·java·数据结构
Henry_He5 小时前
[小技巧]Ubuntu右键快速在浏览器中打开perfetto文件
android
jsync5 小时前
android远程控制
android