一、为什么Android网络请求要用到Flow?
1.1 传统网络请求的痛点
做Android开发时,网络请求是绕不开的日常操作。早年用AsyncTask、回调函数来做请求,代码很容易变得乱七八糟——比如一个请求嵌套另一个请求,就会变成“回调地狱”,改起来像拆盲盒,还经常漏掉生命周期处理:比如Activity页面销毁了,后台请求还在跑,结果返回时报空指针,直接把应用搞崩。后来RxJava虽解决了部分问题,但它的操作符太多,新手要花很久才能搞懂,而且官方后来明确推荐用Kotlin Flow替代RxJava处理轻量数据流。
1.2 Flow的天生优势
Flow是Kotlin协程里的数据流,天生针对Android优化。它是“冷流”,只有当有人订阅它的时候才会跑起来,刚好适配Android的生命周期:比如在ViewModel里启动Flow,页面销毁时协程自动取消,不会泄漏内存。而且它用挂起函数写逻辑,代码是顺序执行的,不像回调那样跳来跳去,维护成本低。加上自带了几十种操作符,处理网络请求的各种场景(比如重试、防抖、错误捕获)不用自己造轮子。
二、Flow在Android网络请求的实际示例
2.1 基础网络请求Flow示例
先明确本次示例的技术栈:Kotlin 1.8 + Coroutines 1.6 + Flow + Retrofit 2.9 + Android X
// Repository层:数据来源的统一管理,负责调用网络或本地数据库
class UserRepository(private val apiService: ApiService) {
// 从网络获取用户信息,返回Flow数据流
fun fetchUserInfo(userId: String) = flow {
// 挂起执行网络请求,不会阻塞主线程
val response = apiService.getUserById(userId)
// 处理请求结果:成功就发射数据,失败抛异常
if (response.isSuccessful && response.body() != null) {
emit(response.body()!!) // 把结果发给下游
} else {
throw Exception("获取用户信息失败,错误码:${response.code()}")
}
}
// 指定Flow运行在IO线程,网络请求必须在后台线程
.flowOn(Dispatchers.IO)
}
// 定义 Retrofit 接口,描述网络请求
interface ApiService {
@GET("https://api.example.com/user/{userId}")
suspend fun getUserById(@Path("userId") userId: String): Response<User>
}
// 简化的User数据类,存储用户信息
data class User(val id: String, val name: String, val avatarUrl: String)
这个示例里,Flow的核心是flow{}构建器,里面用suspend函数处理耗时的网络操作,flowOn指定了线程,完全符合Android主线程不做耗时操作的规范。
2.2 多接口串联的Flow用法
实际开发中,很多请求是依赖的:比如要先拿到用户ID,才能查这个用户的订单。这时候用Flow的flatMapConcat操作符,能顺序执行请求,代码比嵌套回调清晰10倍:
class UserRepository(private val apiService: ApiService) {
// 省略fetchUserInfo方法...
// 获取用户订单,依赖先获取用户的ID
fun fetchUserOrders(userId: String) = fetchUserInfo(userId)
// 上游返回User后,用它的ID请求订单,返回新的Flow
.flatMapConcat { user ->
apiService.getOrdersByUserId(user.id).asFlow()
}
.flowOn(Dispatchers.IO)
}
flatMapConcat的意思是“上游发射一个数据,就生成一个新的Flow,依次执行不重叠”,刚好适合这种有先后顺序的依赖请求,不会出现请求乱序的问题。
三、数据流的错误处理方案
网络请求不可能每次都成功:要么是网络断了,要么是服务器报错,要么是参数错了。Flow自带的错误处理操作符,能让这些异常处理变得更清晰。
3.1 基础错误捕获
最常用的是catch操作符,它能捕获Flow链里所有上游的异常,不会让整个Flow崩溃,还可以给下游返回默认的 fallback 数据,避免UI层收不到数据:
fun fetchUserInfoSafely(userId: String) = flow {
val response = apiService.getUserById(userId)
if (response.isSuccessful && response.body() != null) {
emit(response.body()!!)
} else {
throw Exception("获取用户失败")
}
}
.flowOn(Dispatchers.IO)
// 捕获所有上游异常,处理后返回默认用户
.catch { e ->
Log.e("FlowError", "请求用户出错:${e.message}")
emit(User("0", "默认用户", "")) // 发射默认值,UI能正常更新
}
这样就算网络请求失败,UI也能拿到默认的占位数据,不会出现空白页。
3.2 临时错误重试策略
有时候失败是临时的:比如用户从地下室出来,网络刚恢复,再试一次就好。这时候用retry操作符,可以自动重试指定次数,还能加过滤条件,只重试临时异常:
fun fetchUserInfoWithRetry(userId: String) = flow {
val response = apiService.getUserById(userId)
if (response.isSuccessful && response.body() != null) {
emit(response.body()!!)
} else {
throw Exception("请求失败")
}
}
.flowOn(Dispatchers.IO)
// 最多重试3次,只对网络IO异常重试(比如网络断开、超时)
.retry(3) { cause ->
cause is IOException
}
.catch { e ->
Log.e("FlowError", "重试3次仍失败:${e.message}")
emit(User("0", "网络异常,请稍后再试", ""))
}
这里的retry参数可以是次数,也可以是判断逻辑,只重试IO异常(临时问题),不会重试服务器500这种确定的错误,避免给服务器增加负担。
3.3 自定义错误类型处理
如果要区分不同的错误类型,给用户不同的提示,比如“用户ID无效”“没网络”“服务器挂了”,可以用密封类(sealed class)封装自定义异常:
// 自定义错误密封类,区分不同请求错误
sealed class UserError(message: String) : Exception(message)
class ParamError : UserError("用户ID格式不对")
class NetworkError : UserError("网络连接失败")
class ServerError(code: Int) : UserError("服务器错误,码:$code")
// 带自定义错误的请求方法
fun fetchUserInfoWithCustomError(userId: String) = flow {
when {
userId.isBlank() -> throw ParamError() // 空ID抛参数错
else -> {
val response = apiService.getUserById(userId)
when {
response.code() == 404 -> throw ServerError(404)
!response.isSuccessful -> throw ServerError(response.code())
response.body() != null -> emit(response.body()!!)
else -> throw ServerError(response.code())
}
}
}
}
.flowOn(Dispatchers.IO)
// 捕获不同自定义错误,返回对应提示的用户
.catch { e ->
when (e) {
is ParamError -> emit(User("0", "用户ID不对,请检查"))
is NetworkError -> emit(User("0", "没网络啦,快看看连接"))
is ServerError -> emit(User("0", "服务器忙,稍后再试"))
else -> emit(User("0", "未知错误"))
}
}
这种方式让错误处理更灵活,不同的错误给用户的提示不一样,体验比统一提示好很多。
四、Flow在Android网络请求的典型应用场景
除了基础的单请求和依赖请求,Flow还能解决很多实际开发的高频场景:
典型场景1:搜索防抖
做搜索功能时,用户每输入一个字就发请求,会给服务器造成压力,体验也差。用Flow的debounce(防抖)和flatMapLatest组合,能完美解决:
// SearchViewModel里的搜索方法,简化示例
fun searchUsers(query: String) = flow { emit(query) }
.debounce(300) // 300ms内没有新输入才发射,避免频繁请求
.flatMapLatest { q -> // 上游发新搜索词,就取消之前未完成的请求,只保留最新的
if (q.length >= 2) {
apiService.searchUsers(q).asFlow()
} else {
flowOf(emptyList()) // 输入不够2字返回空列表
}
}
.flowOn(Dispatchers.IO)
这个场景里,flatMapLatest还能避免旧请求结果覆盖新结果的问题,比如用户快速输入“abc”,只会发送最后一次输入的请求,旧的“a”“ab”请求都会被取消。
典型场景2:列表刷新/加载更多
下拉刷新和上拉加载更多,本质是每次触发就发射新的数据流,用Flow的collect订阅,就能自动更新列表数据,配合ViewModel的生命周期,不会出现请求泄漏。
五、技术优缺点和注意事项
5.1 优点
- 代码结构清晰:用挂起函数写逻辑,没有回调嵌套,维护成本低;
- 生命周期安全:冷流的特性,结合ViewModelScope,页面销毁自动取消请求,不会泄漏内存;
- 操作符丰富:错误处理、重试、防抖等场景都有现成操作符,不用自己造轮子;
- 官方主推:和Jetpack组件(ViewModel、Lifecycle)深度集成,是Android开发的官方推荐方案。
5.2 缺点
- 生态不如RxJava:部分复杂场景的封装方案不如RxJava成熟,需要自己整理最佳实践;
- 学习成本:新手理解冷流、操作符的逻辑需要时间,比传统回调的门槛稍高;
- 旧项目迁移成本:如果项目已经用RxJava,全量替换Flow需要评估工作量。
5.3 注意事项
- 线程切换:网络请求必须用
flowOn(Dispatchers.IO),不要在主线程或默认线程做耗时操作; - 错误处理:必须加
catch操作符,避免异常未被捕获导致应用崩溃; - 订阅时机:Flow是冷流,没有调用
collect就不会执行,要在合适的时机(比如页面创建、ViewModel初始化)订阅; - 冷流和热流的区分:网络请求用冷流(每次订阅重新执行),监听网络状态等场景用热流(比如
MutableStateFlow),不要搞混。
六、总结
Kotlin Flow在Android网络请求中的应用,解决了传统回调和RxJava的很多痛点,代码易读、生命周期安全,配合错误处理操作符能应对大部分网络请求场景。从基础的单请求,到多依赖请求,再到搜索防抖这种复杂场景,Flow都能很好的适配。只要掌握核心操作符和错误处理的逻辑,就能写出稳定、易维护的网络请求代码,是现在Android开发的首选方案之一。
评论
围绕“Kotlin Flow在Android网络请求中的应用与数据流错误处理方案”参与讨论