6、网络层构建:Retrofit + OkHttp + Kotlin协程的封装实践

网络层这东西,说白了就是App的血管。数据从服务器流到客户端,中间但凡堵一点,用户就炸了。我这些年接手过的项目,十有八九网络层都是一团乱麻——回调嵌套、线程切换混乱、异常处理靠try-catch硬扛。嗯,今天我们就来聊聊怎么用Retrofit + OkHttp + Kotlin协程,把这层血管彻底打通。

6.1 为什么选这三件套?

你可能会问:现在网络库那么多,Ktor、Volley、甚至原生HttpURLConnection,为什么偏偏是Retrofit+OkHttp?

我个人习惯是这么看的:分工明确,各司其职

  • Retrofit:负责接口定义和序列化。你写个interface,加几个注解,它就帮你把HTTP请求封装好了。说白了就是「声明式网络请求」。
  • OkHttp:负责底层传输。连接池、拦截器、缓存、重试……这些脏活累活它全包了。我在项目中遇到过好几次DNS解析慢的问题,OkHttp的拦截器一把梭就解决了。
  • Kotlin协程:负责异步和生命周期。不用再写回调地狱,suspend函数一把梭,配合ViewModel的viewModelScope,页面销毁自动取消请求。

这三者组合起来,就像一支配合默契的球队:Retrofit是前锋,负责射门(发请求);OkHttp是中场,负责组织(拦截、重试);协程是守门员,负责兜底(异常、取消)。

核心原则:网络层应该是「可替换的」。哪天你想从Retrofit换成Ktor,只需要改封装层,业务代码一行不动。这就是Clean架构的边界感。

6.2 基础架构:从零搭一个网络层

我们先画一张图,看看整个网络层长什么样。别急,我亲手画了一张SVG,帮你理清脉络。

网络层架构总览 业务层(ViewModel / Repository) 调用 suspend 函数,无需关心线程切换 网络封装层(NetworkModule) 统一异常处理、Token刷新、日志拦截 提供 Result 包装类,避免裸抛异常 Retrofit 接口层(ApiService) @GET、@POST、@Path、@Query…… 声明式API OkHttp 底层(拦截器、连接池、缓存)

这张图从上到下,就是一次网络请求的完整链路。业务层只管调用,封装层负责兜底,Retrofit和OkHttp各司其职。接下来我们一步步实现。

6.3 第一步:定义ApiService接口

先写一个接口。这是Retrofit最爽的地方——你只需要声明,不需要实现。

interface ApiService {

    @GET("user/{id}")
    suspend fun getUser(@Path("id") id: Long): BaseResponse<UserBean>

    @POST("login")
    suspend fun login(@Body request: LoginRequest): BaseResponse<LoginBean>

    @GET("list")
    suspend fun getList(
        @Query("page") page: Int,
        @Query("size") size: Int = 20
    ): BaseResponse<List<ItemBean>>
}

注意看,每个方法都是suspend函数。这意味着调用方可以在协程里直接调,不用写Callback。我在项目中遇到过有人把suspend去掉,结果回调嵌套又回来了……嗯,千万别手滑。

小技巧:BaseResponse 是统一的数据包装。后端返回的JSON通常长这样:{ "code": 0, "message": "success", "data": {...} }。我们定义一个泛型类,把code和message抽出来统一处理,业务层只关心data。

6.4 第二步:OkHttp配置与拦截器

OkHttp的拦截器是灵魂。我习惯加三个拦截器:

  1. 日志拦截器:调试时打印请求和响应,上线时关掉。
  2. Token拦截器:自动在请求头里塞Token,遇到401自动刷新。
  3. 缓存拦截器:无网络时走缓存,提升用户体验。

代码长这样:

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = if (BuildConfig.DEBUG) 
            HttpLoggingInterceptor.Level.BODY 
        else 
            HttpLoggingInterceptor.Level.NONE
    })
    .addInterceptor(TokenInterceptor())  // 自定义
    .addInterceptor(CacheInterceptor())  // 自定义
    .cache(Cache(cacheDir, 10L * 1024 * 1024)) // 10MB缓存
    .build()

TokenInterceptor怎么写?我直接贴核心逻辑:

class TokenInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        // 如果不需要Token,直接放行
        if (originalRequest.header("No-Auth") != null) {
            return chain.proceed(originalRequest)
        }
        // 添加Token
        val token = TokenManager.getToken()
        val newRequest = originalRequest.newBuilder()
            .header("Authorization", "Bearer $token")
            .build()
        val response = chain.proceed(newRequest)
        // 如果返回401,尝试刷新Token
        if (response.code == 401) {
            // 同步刷新Token(注意死锁问题)
            val newToken = refreshTokenSync()
            if (newToken != null) {
                response.close()
                val retryRequest = originalRequest.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
                return chain.proceed(retryRequest)
            }
        }
        return response
    }
}

避坑指南:我曾经在Token刷新这里踩过大坑——刷新Token时又触发了TokenInterceptor,导致死循环。解决方案是:刷新Token的请求不加Authorization头,或者在拦截器里加一个「正在刷新」的标记位,用AtomicBoolean控制并发。

6.5 第三步:Retrofit实例与协程适配

创建Retrofit实例时,记得加上协程适配器。没有它,suspend函数就废了。

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配
    .build()

val apiService = retrofit.create(ApiService::class.java)

不过现在Retrofit 2.6.0以上版本已经原生支持suspend了,不需要额外加CoroutineCallAdapterFactory。如果你用的是新版本,直接写就行。

6.6 第四步:统一封装——Result类与异常处理

网络请求不可能永远成功。我最烦的就是到处try-catch。所以,我习惯封装一个Result<T>类:

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val code: Int, val message: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

然后在Repository里统一调用:

class UserRepository(private val api: ApiService) {

    suspend fun getUser(id: Long): Result<UserBean> {
        return try {
            val response = api.getUser(id)
            if (response.code == 0) {
                Result.Success(response.data)
            } else {
                Result.Error(response.code, response.message)
            }
        } catch (e: Exception) {
            // 网络异常、超时、解析失败……全部兜住
            Result.Error(-1, e.message ?: "未知错误")
        }
    }
}

ViewModel里调用时,直接观察Result:

viewModelScope.launch {
    val result = userRepository.getUser(1001)
    when (result) {
        is Result.Success -> // 更新UI
        is Result.Error -> // 显示错误提示
        is Result.Loading -> // 显示加载中
    }
}

这样写的好处是:业务层永远不直接接触异常。所有异常都在Repository层被吞掉,转换成Error状态。UI层只需要关心三种状态:成功、失败、加载中。

6.7 第五步:依赖注入与单例管理

我习惯用Hilt或Koin管理网络层的单例。以Hilt为例:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE
            })
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

这样,整个App里ApiService只有一个实例,OkHttp的连接池也能复用。我在项目中见过有人每次请求都new一个OkHttpClient……那性能,啧啧,不忍直视。

6.8 完整调用链路演示

最后,我们串起来跑一遍。假设用户点击了「获取用户信息」按钮:

  1. ViewModel调用userRepository.getUser(id)
  2. Repository调用apiService.getUser(id),这是一个suspend函数
  3. Retrofit将接口调用转为OkHttp的Request
  4. OkHttp经过拦截器链:日志→Token→缓存→最终发出请求
  5. 服务器返回JSON,Retrofit用Gson反序列化为BaseResponse<UserBean>
  6. Repository检查code,包装成Result.Success或Result.Error
  7. ViewModel收到Result,更新UI状态

整个过程,业务层只写了三行代码。这就是封装的力量。

总结一下:网络层封装的核心就三件事——统一异常、统一数据格式、统一生命周期管理。做到了这三点,你的网络层就能经得起项目迭代的考验。我见过太多项目因为网络层没封装好,后期改个Base URL都要改几十个文件……那滋味,谁改谁知道。

好了,这一章就到这里。代码都在上面了,你可以直接拿去用。记住:网络层是App的血管,别让它堵了。


公众号:蓝海资料掘金营,微信deep3321