4. 网络层封装:Retrofit + OkHttp + Kotlin协程的统一错误处理

网络层这东西,说白了就是App的血管。血管堵了,整个应用都得瘫痪。我在做企业级项目重构时,发现很多团队的网络层代码写得像蜘蛛网——到处是try-catch,每个页面都在处理网络错误,而且处理方式还不一样。嗯,这显然不行。

今天我们就聊聊怎么用Retrofit + OkHttp + Kotlin协程,搭一套干净利落的网络层。重点是统一错误处理,让所有网络请求的错误都走同一个通道。

4.1 为什么需要统一错误处理?

你想想看,一个App里可能有几十个网络请求。如果每个请求都自己写错误处理,会出现什么情况?

  • 有的页面弹Toast,有的弹Dialog,有的直接静默失败
  • 401错误有的跳登录,有的直接崩溃
  • 超时、断网、服务器500,每个开发者的处理方式都不一样

我曾经在一个项目里看到过7种不同的网络错误处理方式。那感觉,就像走进了一个装修风格完全不统一的房子——客厅是欧式,卧室是日式,厕所是工业风。维护起来简直要命。

统一错误处理的核心目标就三个:

  1. 所有网络错误集中处理——不管哪里发请求,错误都走同一个逻辑
  2. 业务错误和网络错误分离——服务器返回的业务异常(比如“余额不足”)和网络异常(比如“连接超时”)分开处理
  3. 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层。

我曾经踩过的坑:在拦截器里直接弹Toast或Dialog。千万别这么做!拦截器运行在子线程,不能直接操作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("未知网络错误"))
            }
        }
    }
}
小技巧:这里用async而不是launch,是因为我们需要返回Deferred,这样上层可以用await()拿到结果。配合协程的async/await模式,代码会非常简洁。

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数据库,搭一套多级缓存方案?到时候再细说。