OkHttp 和 Retrofit 封装使用

背景

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)不应该重试------业务上已经失败了,重试没意义。生产环境建议只重试 IOException5xx

第四层:定义 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": nullJsonSyntaxException 直接报错。要么实体类全用可空类型,要么切 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 层解决,不影响接口定义。

相关推荐
CYY951 天前
OkHttp 的使用
okhttp
CYY953 天前
Retrofit 的使用
retrofit
朝星12 天前
Android开发[14]:网络优化之OkHttp
android·okhttp·kotlin
之歆14 天前
Promise 基础技术深度解析:从回调地狱到链式调用
前端·okhttp·promise
之歆14 天前
Ajax 基础技术深度解析:XHR 从入门到跨域
前端·ajax·okhttp
YHHLAI15 天前
Ajax — 异步数据交互
ajax·okhttp·交互
pyz66620 天前
Retrofit 源码分析
android·retrofit
Xaire24 天前
行行查案例-数据解密-国密s4-webpack打包模块补齐
okhttp
霸道流氓气质24 天前
Spring AI Ollama 连接超时问题排查与解决:OkHttp 读超时配置全指南
人工智能·spring·okhttp