4. 网络层封装:Retrofit + OkHttp + Kotlin协程的统一错误处理
网络层这东西,说白了就是App的血管。血管堵了,整个应用都得瘫痪。我在做企业级项目重构时,发现很多团队的网络层代码写得像蜘蛛网——到处是try-catch,每个页面都在处理网络错误,而且处理方式还不一样。嗯,这显然不行。
今天我们就聊聊怎么用Retrofit + OkHttp + Kotlin协程,搭一套干净利落的网络层。重点是统一错误处理,让所有网络请求的错误都走同一个通道。
4.1 为什么需要统一错误处理?
你想想看,一个App里可能有几十个网络请求。如果每个请求都自己写错误处理,会出现什么情况?
- 有的页面弹Toast,有的弹Dialog,有的直接静默失败
- 401错误有的跳登录,有的直接崩溃
- 超时、断网、服务器500,每个开发者的处理方式都不一样
我曾经在一个项目里看到过7种不同的网络错误处理方式。那感觉,就像走进了一个装修风格完全不统一的房子——客厅是欧式,卧室是日式,厕所是工业风。维护起来简直要命。
统一错误处理的核心目标就三个:
- 所有网络错误集中处理——不管哪里发请求,错误都走同一个逻辑
- 业务错误和网络错误分离——服务器返回的业务异常(比如“余额不足”)和网络异常(比如“连接超时”)分开处理
- UI层无感知——ViewModel或Activity不需要关心网络请求的具体错误细节
4.2 架构设计:三层错误处理模型
我个人习惯把网络错误处理分成三层:
| 层级 | 职责 | 处理位置 |
|---|---|---|
| 第一层:OkHttp拦截器 | 处理HTTP协议级别的错误(401、403、500等) | OkHttp的Interceptor |
| 第二层:Retrofit适配器 | 处理请求过程中的异常(超时、断网、解析失败等) | 自定义CallAdapter.Factory |
| 第三层:业务层封装 | 处理服务器返回的业务错误码(10001、10002等) | Repository层或UseCase层 |
这三层各司其职,互不干扰。第一层处理的是“能不能连上”,第二层处理的是“连上了但出错了”,第三层处理的是“连上了也返回了,但业务逻辑有问题”。
4.3 第一层:OkHttp拦截器处理HTTP错误
先看代码。我习惯在OkHttp的Interceptor里统一处理HTTP状态码:
class HttpErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
when (response.code) {
401 -> {
// 统一处理未授权
// 比如:跳转登录页、清除token
throw UnauthorizedException("登录已过期,请重新登录")
}
403 -> {
throw ForbiddenException("没有权限访问")
}
500, 502, 503 -> {
throw ServerException("服务器繁忙,请稍后重试")
}
}
return response
}
}
这里要注意一点:拦截器里抛出的异常,会被Retrofit的CallAdapter捕获。所以不用担心异常会直接崩到UI层。
4.4 第二层:自定义CallAdapter处理请求异常
这一层是核心。我们需要定义一个统一的返回类型,把成功和失败都包进去:
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val exception: AppException) : ApiResult<Nothing>()
}
// 自定义异常类
sealed class AppException(message: String) : Exception(message) {
class NetworkException(message: String) : AppException(message)
class TimeoutException(message: String) : AppException(message)
class ServerException(message: String) : AppException(message)
class UnauthorizedException(message: String) : AppException(message)
class BusinessException(val code: Int, message: String) : AppException(message)
}
然后自定义CallAdapter,把Retrofit的Call转换成我们自己的ApiResult:
class ApiResultCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Deferred<ApiResult<T>>> {
override fun responseType(): Type = responseType
override fun adapt(call: Call<T>): Deferred<ApiResult<T>> {
return GlobalScope.async {
try {
val response = call.execute()
if (response.isSuccessful) {
ApiResult.Success(response.body()!!)
} else {
// 解析错误体中的业务错误码
val errorBody = response.errorBody()?.string()
val businessException = parseBusinessError(errorBody, response.code())
ApiResult.Error(businessException)
}
} catch (e: IOException) {
ApiResult.Error(AppException.NetworkException("网络连接失败"))
} catch (e: SocketTimeoutException) {
ApiResult.Error(AppException.TimeoutException("请求超时"))
} catch (e: Exception) {
ApiResult.Error(AppException.NetworkException("未知网络错误"))
}
}
}
}
4.5 第三层:业务层统一处理
到了业务层,我们只需要关注ApiResult就行了:
class UserRepository(private val api: UserApi) {
suspend fun getUserInfo(): ApiResult<UserInfo> {
return api.getUserInfo().await()
}
}
// 在ViewModel中使用
class UserViewModel : ViewModel() {
private val repository = UserRepository(ServiceCreator.create())
private val _userInfo = MutableLiveData<UserInfo>()
val userInfo: LiveData<UserInfo> = _userInfo
private val _error = MutableLiveData<AppException>()
val error: LiveData<AppException> = _error
fun loadUserInfo() {
viewModelScope.launch {
when (val result = repository.getUserInfo()) {
is ApiResult.Success -> {
_userInfo.value = result.data
}
is ApiResult.Error -> {
_error.value = result.exception
// 统一处理错误展示
handleError(result.exception)
}
}
}
}
private fun handleError(exception: AppException) {
when (exception) {
is AppException.UnauthorizedException -> {
// 跳转登录页
}
is AppException.TimeoutException -> {
// 显示重试按钮
}
is AppException.BusinessException -> {
// 显示业务错误提示
}
else -> {
// 显示通用错误提示
}
}
}
}
你看,ViewModel里只需要一个when表达式,就能处理所有类型的错误。而且每个错误类型都有明确的处理逻辑,不会出现遗漏。
4.6 完整配置:OkHttp + Retrofit + 协程
最后,把所有的东西串起来:
object ServiceCreator {
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpErrorInterceptor())
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addCallAdapterFactory(ApiResultCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T {
return retrofit.create(serviceClass)
}
}
核心要点总结:
- OkHttp拦截器处理HTTP状态码,抛出特定异常
- 自定义CallAdapter捕获所有异常,统一包装成ApiResult
- 业务层通过when表达式处理ApiResult,实现错误统一处理
- 协程的async/await让异步代码看起来像同步代码
这套方案我在好几个企业级项目里用过,效果都不错。说白了,就是让错误处理这件事变得可预测、可维护。你想想看,当新同事加入项目,看到所有网络请求都返回ApiResult,所有错误都在一个地方处理,是不是一下子就明白了?
嗯,下一章我们会聊聊缓存策略。网络请求不能每次都去服务器拉数据,那样太慢了。怎么用OkHttp的缓存机制加上Room数据库,搭一套多级缓存方案?到时候再细说。