Retrofit 进阶
本教程共 100 篇 · 第 71 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
71. Retrofit 进阶
本节目标:掌握 Retrofit + OkHttp 的工程化用法,包括拦截器、统一 Header、超时配置、文件下载、错误处理、Result 包装,把网络层从能用到稳。
上一章能发请求了,但实际项目里你会遇到一堆问题:怎么打日志看请求体?怎么自动加 Token?怎么统一处理 401、500?怎么显示下载进度?这些都是这章的内容。
拦截器
拦截器是 OkHttp 的杀手锏。每次请求和响应都会经过拦截器链,你可以在这里改请求、看响应、做埋点。
打个比方,拦截器像快递分拣中心的检查站,每个包裹(请求)进来都要过一遍,你可以贴标签(加 Header)、记录日志、拒绝发往某些地址。
日志拦截器
最常用的就是打日志,看实际发出的请求长啥样。OkHttp 自带 HttpLoggingInterceptor:
import okhttp3.logging.HttpLoggingInterceptor
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val client = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
Level 几档:
NONE:不打日志。BASIC:请求行、响应行。HEADERS:加上所有 Header。BODY:再加上请求体和响应体(最详细,调试用)。
Warning正式版千万别用
BODY,会把 Token、密码这种敏感信息打到日志里。开发用BODY,发布切NONE或BASIC。
Token 拦截器
登录后每个请求都要带 Token,一个个接口加太麻烦。统一在拦截器里加:
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val token = tokenProvider()
val request = if (token != null) {
original.newBuilder()
.header("Authorization", "Bearer $token")
.build()
} else {
original
}
return chain.proceed(request)
}
}
用法:
val client = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor { sessionManager.token })
.build()
Tip用
addInterceptor加的是应用拦截器,在重试之前调用;用addNetworkInterceptor加的是网络拦截器,每次实际网络通信都调一次(含重试)。加 Header 一般用应用拦截器。
统一 Header
如果所有请求都要带某几个固定 Header(比如 Accept: application/json、X-Platform: android),用拦截器或者直接在接口方法上贴 @Headers:
@Headers(
"Accept: application/json",
"X-Platform: android"
)
@GET("posts")
suspend fun listPosts(): List<Post>
单个方法加 Header 用 @Header:
@GET("posts/{id}")
suspend fun getPost(
@Path("id") id: Int,
@Header("Cache-Control") cacheControl: String? = null
): Post
超时配置
OkHttp 默认连接、读、写超时都是 10 秒。上传下载大文件可能不够,调一下:
val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
把 OkHttpClient 传给 Retrofit:
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
Note
OkHttpClient应该全局单例,它自带连接池和线程池,每次 new 会浪费资源。Retrofit 实例也建议单例。
文件下载
下载文件用 ResponseBody:
@GET
@Streaming // 大文件加这个,避免一次性读到内存
suspend fun downloadFile(@Url url: String): ResponseBody
@Url 让调用方传完整 URL,@Streaming 告诉 Retrofit 不要把整个响应体缓冲到内存。
保存到本地并显示进度:
suspend fun download(url: String, dest: File): File = withContext(Dispatchers.IO) {
val body = api.downloadFile(url)
body.byteStream().use { input ->
dest.outputStream().use { output ->
val total = body.contentLength()
var downloaded = 0L
val buffer = ByteArray(8 * 1024)
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
downloaded += read
if (total > 0) {
val percent = (downloaded * 100 / total).toInt()
// 通过 Flow 或 StateFlow 推进度
}
}
}
}
dest
}
Tip进度更新别每读一次就推一次,频率太高 UI 卡。累计 1% 才推一次,或者用 throttle。
错误处理
Retrofit 不成功(HTTP 状态码非 2xx)时会抛 HttpException。配合协程,可以用 try/catch 或者 Result 包装统一处理。
方案一:try/catch
suspend fun fetchPost(id: Int): Post {
return try {
api.getPost(id)
} catch (e: HttpException) {
val code = e.code()
val errorBody = e.response()?.errorBody()?.string()
throw ApiException(code, errorBody)
} catch (e: IOException) {
throw NetworkException(e)
}
}
方案二:Result 包装
定义一个密封结果:
sealed interface ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>
data class Error(val code: Int, val message: String) : ApiResult<Nothing>
data class Exception(val e: Throwable) : ApiResult<Nothing>
}
suspend fun <T> safeCall(block: suspend () -> T): ApiResult<T> {
return try {
ApiResult.Success(block())
} catch (e: HttpException) {
ApiResult.Error(e.code(), e.message())
} catch (e: IOException) {
ApiResult.Exception(e)
}
}
调用:
when (val result = safeCall { api.getPost(1) }) {
is ApiResult.Success -> show(result.data)
is ApiResult.Error -> showError("错误码 ${result.code}")
is ApiResult.Exception -> showError("网络异常")
}
Note这种写法在大型项目里很常见,配合 Repository 层把所有异常都收敛成统一的
ApiResult,UI 层不用关心具体异常类型。
401 自动刷新 Token
Token 过期了,常见做法是拦截器检测 401,刷新 Token 后重试原请求:
class TokenRefreshInterceptor(
private val refreshToken: suspend () -> String?
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code == 401) {
response.close()
// 同步阻塞等刷新结果(实际项目用锁或者阻塞式获取)
val newToken = runBlocking { refreshToken() }
if (newToken != null) {
val newRequest = chain.request().newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(newRequest)
}
}
return response
}
}
Warning
runBlocking在拦截器里要慎用,容易死锁。生产环境一般用 OkHttp 的Authenticator接口,专门给刷新 Token 设计。
class TokenAuthenticator(
private val tokenProvider: () -> String?,
private val refresh: suspend () -> String?
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
// 如果已经重试过,放弃
if (response.request.header("X-Retry") != null) return null
val newToken = runBlocking { refresh() } ?: return null
return response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.header("X-Retry", "1")
.build()
}
}
Authenticator 是 OkHttp 专门为 401 设计的,返回新 Request 就自动重试,返回 null 就放弃。
Retrofit + 协程的异常
suspend 方法返回的对象类型决定了异常传播方式:
- 直接返回
T:HTTP 错误抛HttpException,网络错误抛IOException。 - 返回
Response<T>:不抛异常,错误体现在response.isSuccessful上。
// 抛异常风格
@GET("posts/{id}")
suspend fun getPost(@Path("id") id: Int): Post
// 不抛异常风格
@GET("posts/{id}")
suspend fun getPostRaw(@Path("id") id: Int): Response<Post>
Tip业务码错误(HTTP 200 但 body 里
code=401)Retrofit 不会管,要自己在 Repository 层解析 body 判断。
缓存策略
OkHttp 支持 HTTP 缓存,配一个 Cache 目录就行:
val cacheDir = File(context.cacheDir, "http_cache")
val cache = Cache(cacheDir, 10L * 1024 * 1024) // 10MB
val client = OkHttpClient.Builder()
.cache(cache)
.build()
离线时强制走缓存:
val request = Request.Builder()
.url(url)
.header("Cache-Control", "max-stale=86400") // 缓存有效 1 天
.build()
或者用拦截器根据网络状态切策略:
val offlineInterceptor = Interceptor { chain ->
var request = chain.request()
if (!isOnline()) {
request = request.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=86400")
.build()
}
chain.proceed(request)
}
常见坑
- 日志拦截器漏加:调试看不到请求体,以为是后端没收到。
- Token 拦截器加在
addNetworkInterceptor:重试时会被加多次。一般用addInterceptor。 - 下载大文件不开
@Streaming:OOM,整个文件读进内存。 - 401 刷新死循环:刷新接口本身又 401,无限重试。要加重试次数限制或者标记。
OkHttpClient每次 new:性能差,连接池白搭。
小结
Retrofit 进阶的核心是 OkHttp 拦截器:日志、Token、缓存、401 刷新都能在拦截器里搞定。配合 Response<T> 和密封类做错误统一处理,UI 层只管业务。下载用 @Streaming + 流式写入,超时和缓存都在 OkHttpClient.Builder 配。
下一章讲 JSON 解析的细节,包括 Moshi 和 Gson 的对比、嵌套解析、手动解析。