首页 / Android 入门教程 / Retrofit 进阶

Android 入门教程

Retrofit 进阶

本教程共 100 篇 · 第 71 篇 · 更新于 2026-07-28 · 约 9 分钟阅读

AndroidAndroid 入门教程RetrofitOkHttp拦截器网络协程

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,发布切 NONEBASIC

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/jsonX-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)
}

常见坑

  1. 日志拦截器漏加:调试看不到请求体,以为是后端没收到。
  2. Token 拦截器加在 addNetworkInterceptor:重试时会被加多次。一般用 addInterceptor
  3. 下载大文件不开 @Streaming:OOM,整个文件读进内存。
  4. 401 刷新死循环:刷新接口本身又 401,无限重试。要加重试次数限制或者标记。
  5. OkHttpClient 每次 new:性能差,连接池白搭。

小结

Retrofit 进阶的核心是 OkHttp 拦截器:日志、Token、缓存、401 刷新都能在拦截器里搞定。配合 Response<T> 和密封类做错误统一处理,UI 层只管业务。下载用 @Streaming + 流式写入,超时和缓存都在 OkHttpClient.Builder 配。

下一章讲 JSON 解析的细节,包括 Moshi 和 Gson 的对比、嵌套解析、手动解析。