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,帮你理清脉络。
这张图从上到下,就是一次网络请求的完整链路。业务层只管调用,封装层负责兜底,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
6.4 第二步:OkHttp配置与拦截器
OkHttp的拦截器是灵魂。我习惯加三个拦截器:
- 日志拦截器:调试时打印请求和响应,上线时关掉。
- Token拦截器:自动在请求头里塞Token,遇到401自动刷新。
- 缓存拦截器:无网络时走缓存,提升用户体验。
代码长这样:
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 完整调用链路演示
最后,我们串起来跑一遍。假设用户点击了「获取用户信息」按钮:
- ViewModel调用
userRepository.getUser(id) - Repository调用
apiService.getUser(id),这是一个suspend函数 - Retrofit将接口调用转为OkHttp的Request
- OkHttp经过拦截器链:日志→Token→缓存→最终发出请求
- 服务器返回JSON,Retrofit用Gson反序列化为BaseResponse<UserBean>
- Repository检查code,包装成Result.Success或Result.Error
- ViewModel收到Result,更新UI状态
整个过程,业务层只写了三行代码。这就是封装的力量。
总结一下:网络层封装的核心就三件事——统一异常、统一数据格式、统一生命周期管理。做到了这三点,你的网络层就能经得起项目迭代的考验。我见过太多项目因为网络层没封装好,后期改个Base URL都要改几十个文件……那滋味,谁改谁知道。
好了,这一章就到这里。代码都在上面了,你可以直接拿去用。记住:网络层是App的血管,别让它堵了。