第36章:网络请求(Retrofit)——配置、注解、拦截器与转换器

网络请求,几乎是每个App的标配。在Kotlin世界里,Retrofit就是那个最趁手的工具。我个人用了好几年,从Java时代一路跟到Kotlin,可以说它是我最信赖的HTTP客户端库之一。

这一章,我会带你完整走一遍Retrofit的配置流程。从基础搭建到高级玩法,咱们一次说透。

36.1 Retrofit是什么?为什么选它?

Retrofit是Square公司出品的一个类型安全的HTTP客户端。说白了,它把网络请求变成了接口调用。你只需要定义一个接口,加几个注解,就能发起请求。

它的核心优势有三个:

  • 注解驱动:用注解描述请求方式、路径、参数
  • 自动序列化:配合Gson或Moshi,JSON直接转对象
  • 与OkHttp深度集成:拦截器、缓存、日志,全都能用

我记得刚接触Retrofit时,最让我惊艳的是它的简洁性。以前写网络请求,要手动拼URL、处理JSON、管理线程。用了Retrofit后,代码量直接砍半。

36.2 基础配置:三步搞定

先看一个最基础的配置示例。我习惯把Retrofit实例做成单例,避免重复创建。

// 第一步:定义接口
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: Int): User
}

// 第二步:构建Retrofit实例
object RetrofitClient {
    private const val BASE_URL = "https://api.example.com/"
    
    private val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .build()
    
    val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    
    val apiService: ApiService = retrofit.create(ApiService::class.java)
}

// 第三步:发起请求
suspend fun loadUser() {
    val user = RetrofitClient.apiService.getUser(123)
    println("用户名:${user.name}")
}

这里有几个关键点:

  • baseUrl必须以/结尾,否则会报错。我踩过这个坑,后来就养成了习惯。
  • GsonConverterFactory负责把JSON转成Kotlin对象。如果你用Moshi,就换成MoshiConverterFactory
  • OkHttpClient可以单独配置,拦截器、超时时间都在这里设置。

36.3 注解使用:GET、POST、PUT、DELETE

Retrofit的注解很直观。常用的有这些:

注解 用途 示例
@GET 查询数据 @GET("users")
@POST 新增数据 @POST("users")
@PUT 更新数据 @PUT("users/{id}")
@DELETE 删除数据 @DELETE("users/{id}")
@Path 替换路径参数 @Path("id")
@Query 添加查询参数 @Query("page")
@Body 发送请求体 @Body user: User
@Header 设置请求头 @Header("Authorization")

来看一个完整的例子:

interface ApiService {
    // GET请求,带路径参数和查询参数
    @GET("users/{id}/posts")
    suspend fun getUserPosts(
        @Path("id") userId: Int,
        @Query("page") page: Int = 1,
        @Query("limit") limit: Int = 10
    ): List<Post>
    
    // POST请求,发送JSON body
    @POST("users")
    suspend fun createUser(@Body user: User): User
    
    // 动态URL,用@Url注解
    @GET
    suspend fun fetchFromUrl(@Url url: String): ResponseBody
}

我个人比较喜欢用@Query加默认值的方式。这样调用时可以不传参,代码更简洁。

36.4 OkHttp拦截器:日志、重试、Token刷新

拦截器是OkHttp的灵魂。Retrofit底层用的就是OkHttp,所以拦截器完全通用。

我项目中常用的拦截器有三种:

日志拦截器

调试时必备。能看到请求和响应的完整内容。

val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = if (BuildConfig.DEBUG) {
        HttpLoggingInterceptor.Level.BODY
    } else {
        HttpLoggingInterceptor.Level.NONE
    }
}

Token自动刷新拦截器

我曾经被Token过期问题折磨过。后来写了个拦截器,自动检测401并刷新Token。

class AuthInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val request = originalRequest.newBuilder()
            .header("Authorization", "Bearer ${TokenManager.getToken()}")
            .build()
        
        val response = chain.proceed(request)
        
        // 如果返回401,尝试刷新Token
        if (response.code == 401) {
            // 同步刷新Token
            val newToken = refreshTokenSync()
            if (newToken != null) {
                TokenManager.saveToken(newToken)
                // 重试原始请求
                val retryRequest = originalRequest.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
                response.close()
                return chain.proceed(retryRequest)
            }
        }
        return response
    }
    
    private fun refreshTokenSync(): String? {
        // 这里实现Token刷新逻辑
        return null
    }
}
注意:拦截器里做网络请求要小心。如果多个请求同时触发Token刷新,可能会并发问题。我一般用synchronized或协程的Mutex来保证只有一个刷新请求在执行。

36.5 Gson vs Moshi:选哪个?

这两个都是JSON转换库。Gson更老牌,Moshi是后来者。我两个都用过,说说我的感受。

特性 Gson Moshi
性能 中等 更快(特别是大JSON)
空安全 默认不检查 原生支持Kotlin空安全
自定义适配器 TypeAdapter JsonAdapter(更简洁)
社区生态 非常成熟 正在成长

如果你用Kotlin,我个人更推荐Moshi。它对空安全的支持更好,不会出现Gson那种把null转成空字符串的诡异问题。

Moshi的配置也很简单:

val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()

注意要加KotlinJsonAdapterFactory,否则Moshi不认识Kotlin的数据类。

36.6 核心知识体系

下面这张图,帮你理清Retrofit的整体架构:

Retrofit 核心架构图 应用层(ApiService接口) Retrofit 核心(注解解析 + 动态代理) OkHttp 网络层 转换器(Gson/Moshi) 拦截器链 连接池 / 超时 / 缓存 JSON ↔ 对象 自动转换 日志 / Token刷新 / 重试 注解驱动 → 动态代理 → OkHttp执行 → 转换器解析 → 返回结果 整个流程是链式的,每一层都可以自定义

36.7 避坑指南

最后,分享几个我踩过的坑:

坑1:baseUrl忘记加斜杠

Retrofit要求baseUrl必须以/结尾。否则会抛出IllegalArgumentException。我刚开始用的时候,排查了半天才发现是这个问题。

坑2:协程与回调混用

Retrofit从2.6.0开始支持suspend函数。如果你还在用Call回调,建议尽快迁移。协程的写法更简洁,而且能避免回调地狱。

坑3:Moshi的Kotlin适配器

用Moshi时,别忘了加KotlinJsonAdapterFactory。否则Moshi会把Kotlin数据类当成Java类处理,导致空安全失效。我曾经因为这个,线上出了好几个空指针异常。

小技巧:调试时把日志拦截器级别设为BODY,能看到完整的请求和响应。上线前记得改成NONE,避免敏感信息泄露。

好了,这一章的内容就到这里。Retrofit的配置、注解、拦截器、转换器,咱们都过了一遍。你可以在自己的项目里试试看,从最简单的GET请求开始,慢慢加上拦截器和自定义转换器。实践出真知,多写几次就熟练了。


公众号:蓝海资料掘金营,微信deep3321