背景
OkHttp 和 Retrofit 都出自 Square,两个库各管一摊:OkHttp 负责网络传输------建立连接、发数据、收数据、协议协商;Retrofit 负责把 Java/Kotlin 接口翻译成 HTTP 请求------解析注解、拼接参数、序列化/反序列化、适配返回类型。Retrofit 的底层网络调用全部交给 OkHttp,不直接碰 Socket。
分开来用的话,OkHttp 能独立处理任何 HTTP 场景(文件上传下载、WebSocket 等),Retrofit 则是专注 REST API 的声明式调用。两个凑在一起才是 Android 项目里网络层的标准组合。
依赖
kotlin
// OkHttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
// Moshi 和 Gson 都可以
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
logging-interceptor 只在调试时需要,release 包可以不加,或者在 Interceptor 里判断 BuildConfig.DEBUG 控制日志等级。
使用和封装
直接用在项目里的封装,需要处理几件事:全局单例、公共 Header 注入、超时配置、日志、统一异常处理、协程适配。下面逐层搭起来。
第一层:配置 OkHttpClient
网络层的事全在 OkHttpClient 里配置。Retrofit 只管"调用",网络质量、超时、重试、缓存这些由 OkHttp 决定。
kotlin
object HttpClient {
val instance: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(AuthInterceptor())
.addInterceptor(LoggingInterceptor())
.addInterceptor(CacheInterceptor())
.build()
}
}
三个超时分别控制连接建立、读数据、写数据的等待时间。retryOnConnectionFailure(true) 在连接失败时自动换 IP 重试,对弱网环境有帮助。
第二层:搭建 Retrofit
Retrofit 里把 OkHttpClient 设进去,再配上 Converter 和 baseUrl:
kotlin
object ApiClient {
private const val BASE_URL = "https://api.example.com/"
val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(HttpClient.instance)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
inline fun <reified T> createApi(): T = retrofit.create(T::class.java)
}
client(HttpClient.instance) 把两层连上了。后面通过 retrofit.create() 生成的接口实现,底层走的都是同一个 OkHttpClient 实例,连接池和线程池全局复用。
第三层:公共 Interceptor
AuthInterceptor ------ 统一注入 Header
kotlin
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val request = original.newBuilder()
.header("Authorization", "Bearer ${TokenStore.token}")
.header("Accept", "application/json")
.header("X-Platform", "Android")
.header("X-Version", BuildConfig.VERSION_NAME)
.build()
return chain.proceed(request)
}
}
所有通过 Retrofit 发出的请求,Interceptor 这一层会自动加上 Token 和公共 Header。接口定义里不用再单独写 @Header。
LoggingInterceptor ------ 调试日志
kotlin
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
if (!BuildConfig.DEBUG) return chain.proceed(chain.request())
val request = chain.request()
val startMs = System.currentTimeMillis()
Log.d("HTTP", "--> ${request.method} ${request.url}")
val response = chain.proceed(request)
val duration = System.currentTimeMillis() - startMs
val body = response.peekBody(Long.MAX_VALUE).string()
Log.d("HTTP", "<-- ${response.code} ${request.url} (${duration}ms)")
Log.d("HTTP", body.take(2000)) // 截断,防止大响应撑爆 logcat
return response
}
}
注意 peekBody() 而不是 string()------peekBody 读完之后原始流还在,Retrofit 的 Converter 能继续消费。Release 包直接跳过日志逻辑,不额外开销。
CacheInterceptor ------ 离线缓存
kotlin
class CacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// 无网络时强制走缓存
if (!isNetworkAvailable()) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
val response = chain.proceed(request)
// 有网络时缓存有效期设短
return response.newBuilder()
.header("Cache-Control", if (isNetworkAvailable()) "public, max-age=60" else "public, only-if-cached, max-stale=86400")
.build()
}
}
加上 OkHttp 的磁盘缓存配合使用:
kotlin
val cacheSize = 20 * 1024 * 1024L // 20MB
val cacheDir = File(context.cacheDir, "http_cache")
val cache = Cache(cacheDir, cacheSize)
OkHttpClient.Builder()
.cache(cache)
.addInterceptor(CacheInterceptor())
RetryInterceptor ------ 请求重试
kotlin
class RetryInterceptor(private val maxRetry: Int = 2) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var response: Response
var retryCount = 0
var delayMs = 500L
do {
response = chain.proceed(chain.request())
if (response.isSuccessful || retryCount >= maxRetry) break
response.close()
Thread.sleep(delayMs)
delayMs *= 2
retryCount++
} while (retryCount <= maxRetry)
return response
}
}
这里只对 5xx 或连接层错误做重试,4xx(比如 401/403)不应该重试------业务上已经失败了,重试没意义。生产环境建议只重试 IOException 和 5xx。
第四层:定义 API 接口
有了上面的逻辑,接口定义就很清晰了:
kotlin
// 通用响应体
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T
)
data class User(val id: Long, val name: String, val avatar: String)
// API 接口
interface UserApi {
@GET("user/{id}")
suspend fun getUser(@Path("id") id: Long): ApiResponse<User>
@GET("user/list")
suspend fun getUserList(
@Query("page") page: Int = 1,
@Query("size") size: Int = 20
): ApiResponse<List<User>>
@POST("user/login")
suspend fun login(@Body body: LoginBody): ApiResponse<Token>
@FormUrlEncoded
@POST("user/update")
suspend fun updateProfile(@FieldMap fields: Map<String, String>): ApiResponse<User>
}
Header(Token/Accept/Platform)都在 Interceptor 里统一加了,接口方法里不出现 @Header。只写业务参数。
第五层:统一异常处理
网络层的异常分成几类:HTTP 状态码异常(4xx/5xx)、IO 异常(超时/断网)、业务错误(HTTP 200 但 code != 0),用 sealed class 处理。
kotlin
sealed class NetResult<out T> {
data class Success<T>(val data: T) : NetResult<T>()
data class ApiError(val code: Int, val msg: String) : NetResult<Nothing>()
data class NetError(val e: IOException) : NetResult<Nothing>()
}
suspend fun <T> safeApi(block: suspend () -> T): NetResult<T> {
return try {
NetResult.Success(block())
} catch (e: HttpException) {
val errBody = e.response()?.errorBody()?.string() ?: e.message()
NetResult.ApiError(e.code(), errBody)
} catch (e: IOException) {
NetResult.NetError(e)
}
}
如果后端在 body 里用 code 表示业务结果(而非 HTTP 状态码),把判断加进去:
kotlin
suspend fun <T> safeApi(block: suspend () -> ApiResponse<T>): NetResult<T> {
return try {
val resp = block()
if (resp.code == 0) NetResult.Success(resp.data)
else NetResult.ApiError(resp.code, resp.message)
} catch (e: HttpException) {
val errBody = e.response()?.errorBody()?.string() ?: e.message()
NetResult.ApiError(e.code(), errBody)
} catch (e: IOException) {
NetResult.NetError(e)
}
}
第六层:ViewModel 层消费
kotlin
class UserViewModel : ViewModel() {
private val api = ApiClient.createApi<UserApi>()
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
private val _error = MutableSharedFlow<String>()
val error: SharedFlow<String> = _error.asSharedFlow()
fun loadUser(id: Long) {
viewModelScope.launch {
when (val r = safeApi { api.getUser(id) }) {
is NetResult.Success -> _user.value = r.data
is NetResult.ApiError -> _error.emit(r.msg)
is NetResult.NetError -> _error.emit("网络异常,请检查网络")
}
}
}
}
继续优化 when
kotlin
inline fun <T> NetResult<T>.fold(
onSuccess: (T) -> Unit,
onError: (String) -> Unit
) {
when (this) {
is NetResult.Success -> onSuccess(data)
is NetResult.ApiError -> onError(msg)
is NetResult.NetError -> onError("网络异常,请检查网络")
}
}
调用:
kotlin
safeApi { api.getUser(id) }.fold(
onSuccess = { _user.value = it },
onError = { _error.emit(it) }
)
第七层:多 BaseUrl 场景
一个项目调多个域名,各建一套 Retrofit 但共用 OkHttpClient:
kotlin
object ApiService {
private val okHttp = HttpClient.instance
val main: Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttp)
.addConverterFactory(GsonConverterFactory.create())
.build()
val upload: Retrofit = Retrofit.Builder()
.baseUrl("https://upload.example.com/")
.client(okHttp)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
// 使用
val userApi: UserApi = ApiService.main.create()
val uploadApi: UploadApi = ApiService.upload.create()
Retrofit 实例可以建多个,但 OkHttpClient 保持一个------连接池、线程池、缓存是 OkHttpClient 处理。
文件上传:Retrofit + OkHttp 进度监听
Retrofit 处理文件上传的注解很简洁:
kotlin
interface UploadApi {
@Multipart
@POST("upload")
suspend fun upload(
@Part file: MultipartBody.Part,
@Part("desc") desc: RequestBody
): ApiResponse<String>
}
但 Retrofit 本身没有进度回调。需要借助 OkHttp 的 Interceptor,把请求体包一层计数的壳:
kotlin
class ProgressRequestBody(
private val delegate: RequestBody,
private val onProgress: (bytesWritten: Long, totalBytes: Long) -> Unit
) : RequestBody() {
override fun contentType(): MediaType? = delegate.contentType()
override fun contentLength(): Long = delegate.contentLength()
override fun writeTo(sink: BufferedSink) {
val total = contentLength()
val countingSink = object : ForwardingSink(sink) {
var bytesWritten = 0L
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
bytesWritten += byteCount
onProgress(bytesWritten, total)
}
}
val bufferedSink = countingSink.buffer()
delegate.writeTo(bufferedSink)
bufferedSink.flush()
}
}
然后通过 Interceptor 把原始 RequestBody 替换成 ProgressRequestBody:
kotlin
class UploadProgressInterceptor(
private val onProgress: (Long, Long) -> Unit
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val body = original.body ?: return chain.proceed(original)
val progressBody = ProgressRequestBody(body, onProgress)
return chain.proceed(original.newBuilder().method(original.method, progressBody).build())
}
}
上传接口配合一个专用 OkHttpClient 实例(加上进度拦截器),Retrofit 侧不改任何代码:
kotlin
val uploadOkHttp = OkHttpClient.Builder()
.addInterceptor(UploadProgressInterceptor { written, total ->
val percent = (written * 100 / total).toInt()
// 更新进度条
})
.build()
val uploadRetrofit = Retrofit.Builder()
.baseUrl("https://upload.example.com/")
.client(uploadOkHttp)
.addConverterFactory(GsonConverterFactory.create())
.build()
文件下载:Retrofit + OkHttp 流式写盘
下载接口用 @Streaming 注解,避免把整个文件加载进内存:
kotlin
interface DownloadApi {
@Streaming
@GET
suspend fun download(@Url url: String): ResponseBody
}
下载时边读边写:
kotlin
val api = ApiService.main.create<DownloadApi>()
scope.launch(Dispatchers.IO) {
val response = api.download("https://example.com/video.mp4")
if (response.isSuccessful) {
response.body()?.byteStream()?.use { input ->
File("/sdcard/Download/video.mp4").outputStream().use { output ->
input.copyTo(output, bufferSize = 8 * 1024)
}
}
}
}
需要下载进度的话,和上传类似------Interceptor 里把 ResponseBody 包一层 ProgressResponseBody,在 read() 里计数。
Token 自动刷新
401 时自动换 Token 然后重试,这套逻辑放在 OkHttp Interceptor 里,对 Retrofit 完全透明:
kotlin
class TokenRefreshInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var response = chain.proceed(chain.request())
if (response.code == 401) {
synchronized(this) {
val newToken = refreshTokenSync() // 同步阻塞拿新 Token
if (newToken != null) {
response.close()
val newRequest = chain.request().newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
response = chain.proceed(newRequest)
}
}
}
return response
}
private fun refreshTokenSync(): String? {
// 用一个新的 OkHttpClient 发刷新请求,避免用当前 client 造成循环
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.example.com/token/refresh")
.post(FormBody.Builder().add("refresh_token", TokenStore.refreshToken).build())
.build()
val resp = client.newCall(request).execute()
return if (resp.isSuccessful) {
val newToken = Gson().fromJson(resp.body?.string(), Token::class.java).accessToken
TokenStore.token = newToken
newToken
} else null
}
}
synchronized 保证并发时只刷一次 Token,避免了十来个请求同时收到 401 然后全部去刷 Token 的浪费。
refreshTokenSync()里新建了一个OkHttpClient(),这是少数合理的临时创建场景------否则会造成拦截器的递归调用。临时实例用execute()同步请求,不会泄漏。
整体架构一览
kotlin
┌────────────────────────────┐
│ ViewModel / UI │
├────────────────────────────┤
│ safeApi() + NetResult │ ← 统一异常处理 + sealed class
├────────────────────────────┤
│ Retrofit (interface) │ ← 声明式接口 + Converter
├────────────────────────────┤
│ OkHttp Interceptor 链 │ ← Auth / Log / Cache / Retry / TokenRefresh
├────────────────────────────┤
│ OkHttp Core │ ← 连接池 / 分发器 / 缓存 / DNS / TLS
├────────────────────────────┤
│ TCP / TLS │
└────────────────────────────┘
常见问题
OkHttpClient 多实例导致连接池失效
OkHttpClient 每 new 一个就带一套独立的连接池和线程池。如果在 Retrofit 的 Builder 里每次都 new OkHttpClient(),连接复用和缓存全废了。保持全局一个实例。
Interceptor 里把 ResponseBody 读了两遍
Interceptor 里打完日志,结果 Retrofit 的 Converter 拿到的流是空的------string() 只能读一次。要么用 peekBody()(只是把内容缓存到内存,原始流还在),要么先 string() 存起来再构造新的 ResponseBody。
Converter 顺序导致反序列化失败
Retrofit 按注册顺序尝试 Converter.Factory。如果把 ScalarsConverterFactory 放在 GsonConverterFactory 前面,Scalars 的 responseBodyConverter 对 String 返回非 null,Gson 就没机会了。特殊 Converter 放前面不假,但要注意别让过于宽泛的类型把后面的 Converter 给挡了。
Token 刷新期间请求堆积
不使用 synchronized 的话,多个并发请求同时收到 401,会同时去刷新 Token,浪费且可能产生竞态。synchronized 保证只刷一次,刷新期间其他请求在锁上排队。
协程取消后 OkHttp Call 仍在跑
Retrofit 的 suspend 函数在协程取消时会自动 cancel 底层 OkHttp Call。但如果用 Call.enqueue() 或者手动 withContext 切换线程、不检查 isActive,就会出现协程取消了但请求还在跑的情况。直接用 Retrofit 的 suspend 接口,别在 suspend 方法里再包线程切换。
Gson 反序列化 null 到非空字段
Kotlin 里 val data: T 声明非空,Gson 不管这个。接口返回 "data": null 时 JsonSyntaxException 直接报错。要么实体类全用可空类型,要么切 Moshi。新项目建议 Moshi。
多个 Retrofit 实例是否共用连接池
只要用的是同一个 OkHttpClient 实例,不管建多少个 Retrofit 对象,底层连接池都是共用的。关键在 OkHttpClient 是不是同一个。
总结
OkHttp 和 Retrofit 配合的要点就一句话:Retrofit 管接口定义和序列化,OkHttp 管传输质量和扩展。两层各司其职,通过 retrofitBuilder.client(okHttpClient) 接在一起。
封装路径建议按照这种方式搭:先搞定 OkHttpClient 的单例和超时配置 → 加上 Auth/Logging/Cache 这几个 Interceptor → 交给 Retrofit 作为 client → 定义 API 接口 → 用 safeApi 处理异常 → ViewModel 里 fold 消费结果。文件上传下载的进度、Token 自动刷新这些"进阶需求"全部在 Interceptor 层解决,不影响接口定义。