Retrofit 的使用

Retrofit 是什么

Retrofit 是 Square 出的一个 HTTP 客户端,2013 年开源。严格来说它自己不发网络请求------请求走 OkHttp,JSON 解析走 Gson/Moshi,Retrofit 做的事情很简单:把你的 interface 翻译成 HTTP 调用。你写一个接口方法,贴几个注解,它运行时给你把实现类生成了。

这个库在 Android 圈基本是默认选项了。不是因为它多新奇,是因为确实好用------声明式、可插拔、跟 OkHttp 无缝配合。这么多年过去了,能打的替代品不多,Ktor 算一个,但生态和上手成本还差点意思。

接入项目

Retrofit 本身只是一个壳,真正的网络请求靠 OkHttp,JSON 解析靠 Converter。所以引入的时候至少三个依赖:

kotlin 复制代码
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")

Groovy 写法:

groovy 复制代码
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'

Gson

Retrofit 不关心你用哪个 JSON 库,它只认 Converter.Factory 接口。Gson 是 Google 维护的,Android 项目里几乎都会集成。建一个 GsonConverterFactory 塞进去就能用:

kotlin 复制代码
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

零配置,跑得快,社区资源多------大部分 Retrofit 教程和 StackOverflow 答案都是 Gson 的,出问题好搜。

但 Gson 对 Kotlin 不友好。它不认识 Kotlin 的 null-safety,接口返回 "data": null 时,你的实体类字段声明了 val data: T(非空),Gson 照样往里塞 null,然后 JsonSyntaxException 炸得你一脸懵。默认参数也是不生效的,val page: Int = 1 这种后端没返的字段,出来是 0 不是 1。

解法有几个:实体类字段全声明成可空、自己写 TypeAdapter 、或者使用 Moshi。

Moshi

Moshi 也是 Square 出的,跟 Retrofit 同源。它的设计从一开始就考虑了 Kotlin,配合 moshi-kotlin 扩展,null-safety、默认参数、无参构造函数这些 Kotlin 特性都工作正常。反序列化失败时给的错误信息也比 Gson 清楚很多------Gson 经常只甩一句 "Expected BEGIN_OBJECT but was STRING",Moshi 会告诉你哪个字段、哪一行出了问题。

依赖:

kotlin 复制代码
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.1")

构建:

kotlin 复制代码
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()

注意 KotlinJsonAdapterFactory() 这一步是必须的。忘加的话 Moshi 就退化到跟 Gson 差不多的体验------null 乱入、默认参数不生效全回来了。

注解方面:如果你之前的实体类用 Gson 的 @SerializedName("xxx"),切 Moshi 后改成 @field:Json(name = "xxx")。数据类结构不用动。

接手的老项目里如果全是 Gson,换 Moshi 收益有限,不建议折腾。新项目可以直接使用 Moshi,序列化相关的坑少踩一半。

Kotlin Serialization

如果你追求编译期安全、零反射开销,还有 Kotlin Serialization:

kotlin 复制代码
implementation("com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")

val json = Json { ignoreUnknownKeys = true }
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
    .build()

编译期生成序列化代码,没有运行时反射,性能最好。代价是每个实体类都得加 @Serializable,还得配 Kotlin 编译器插件。

Call Adapter

Retrofit 2.6 之后内置了协程支持,方法声明 suspend 就直接跑协程,不需要额外引入 Call Adapter。RxJava 项目才需要:

kotlin 复制代码
implementation("com.squareup.retrofit2:adapter-rxjava3:2.11.0")

版本管理

多模块项目把版本号抽到 libs.versions.toml,避免不同模块版本不一致。这个比较基础,写法不展开了。

基础用法

先从最简单的一发 GET 开始:

kotlin 复制代码
data class User(val id: Long, val name: String, val avatar: String)

data class ApiResponse<T>(val code: Int, val message: String, val data: T)

interface UserApi {
    @GET("user/{id}")
    suspend fun getUser(@Path("id") userId: Long): ApiResponse<User>
}

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val userApi = retrofit.create(UserApi::class.java)

调用:

kotlin 复制代码
scope.launch {
    val result = userApi.getUser(123)
}

然后说几种常见的传参方式。

GET 请求传参

kotlin 复制代码
interface SearchApi {
    // 单个 Query
    @GET("search")
    suspend fun search(@Query("q") keyword: String): ApiResponse<List<Item>>

    // 多个 Query + 默认值
    @GET("search")
    suspend fun search(
        @Query("q") keyword: String,
        @Query("page") page: Int = 1,
        @Query("size") size: Int = 20
    ): ApiResponse<List<Item>>

    // 动态参数用 QueryMap
    @GET("search")
    suspend fun search(@QueryMap params: Map<String, String>): ApiResponse<List<Item>>

    // Path + Query 混用
    @GET("user/{id}/posts")
    suspend fun getUserPosts(
        @Path("id") userId: Long,
        @Query("sort") sort: String = "desc"
    ): ApiResponse<List<Post>>
}

POST / PUT / DELETE

kotlin 复制代码
interface ArticleApi {
    // JSON Body
    @POST("article")
    suspend fun create(@Body article: Article): ApiResponse<Article>

    // 表单
    @FormUrlEncoded
    @POST("login")
    suspend fun login(
        @Field("username") username: String,
        @Field("password") password: String
    ): ApiResponse<Token>

    @FormUrlEncoded
    @POST("profile")
    suspend fun updateProfile(@FieldMap fields: Map<String, String>): ApiResponse<User>

    @PUT("article/{id}")
    suspend fun update(
        @Path("id") articleId: Long,
        @Body article: Article
    ): ApiResponse<Article>

    @DELETE("article/{id}")
    suspend fun delete(@Path("id") articleId: Long): ApiResponse<Unit>
}

Header 传参

kotlin 复制代码
// 固定值
@Headers("Accept: application/json")
@GET("user/me")
suspend fun getMe(): ApiResponse<User>

// 动态值
@GET("user/me")
suspend fun getMe(@Header("Authorization") token: String): ApiResponse<User>

项目里 Header 一般不会在每个接口上写------Token、设备号、版本号这些用 OkHttp Interceptor 统一注入,后面封装部分会说到。

文件上传

kotlin 复制代码
interface UploadApi {
    @Multipart
    @POST("upload")
    suspend fun upload(
        @Part file: MultipartBody.Part,
        @Part("desc") desc: RequestBody
    ): ApiResponse<String>
}

// 调用
val file = File("/sdcard/photo.jpg")
val body = file.asRequestBody("image/jpeg".toMediaType())
val part = MultipartBody.Part.createFormData("file", file.name, body)
val desc = "头像".toRequestBody("text/plain".toMediaType())
uploadApi.upload(part, desc)

动态 URL

服务器返回完整地址、需要直接下载的时候用 @Url

kotlin 复制代码
interface DownloadApi {
    @GET
    suspend fun download(@Url url: String): ResponseBody
}

封装:怎么把 Retrofit 用得舒服

不封装直接用,每个地方都要写 try-catch、判断 code、处理各种异常,写着写着代码就烂了。下面是我项目里实际在用的封装方式------不重,但够用。

统一构建 OkHttp 和 Retrofit

kotlin 复制代码
object ApiClient {

    private const val BASE_URL = "https://api.example.com/"

    val okHttpClient: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .addInterceptor { chain ->
                val req = chain.request().newBuilder()
                    .header("Authorization", "Bearer ${TokenManager.token}")
                    .header("Accept", "application/json")
                    .build()
                chain.proceed(req)
            }
            .also {
                if (BuildConfig.DEBUG) {
                    it.addInterceptor(
                        HttpLoggingInterceptor().apply {
                            level = HttpLoggingInterceptor.Level.BODY
                        }
                    )
                }
            }
            .build()
    }

    private val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    inline fun <reified T> create(): T = retrofit.create(T::class.java)
}

用的时候一行拿到接口实例:

kotlin 复制代码
val userApi = ApiClient.create<UserApi>()

统一异常处理: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>()
    data class UnknownError(val e: Throwable) : NetResult<Nothing>()
}

suspend fun <T> safeCall(call: suspend () -> T): NetResult<T> {
    return try {
        NetResult.Success(call())
    } catch (e: HttpException) {
        // 4xx / 5xx,errorBody 里通常有后端给的错误信息
        val errMsg = e.response()?.errorBody()?.string() ?: e.message()
        NetResult.ApiError(e.code(), errMsg)
    } catch (e: IOException) {
        NetResult.NetError(e)
    } catch (e: Exception) {
        NetResult.UnknownError(e)
    }
}

如果后端习惯 HTTP 200 返回业务错误(body 里 code != 0),把业务判断也加进去:

kotlin 复制代码
suspend fun <T> safeCall(
    call: suspend () -> ApiResponse<T>
): NetResult<T> {
    return try {
        val resp = call()
        if (resp.code == 0) NetResult.Success(resp.data)
        else NetResult.ApiError(resp.code, resp.message)
    } catch (e: HttpException) { ... }
    catch (e: IOException) { ... }
    catch (e: Exception) { ... }
}

ViewModel 里调用:

kotlin 复制代码
class UserViewModel : ViewModel() {
    private val api = ApiClient.create<UserApi>()

    private val _user = MutableStateFlow<User?>(null)
    val user = _user.asStateFlow()

    private val _toast = MutableSharedFlow<String>()
    val toast = _toast.asSharedFlow()

    fun loadUser(id: Long) {
        viewModelScope.launch {
            when (val r = safeCall { api.getUser(id) }) {
                is NetResult.Success -> _user.value = r.data.data
                is NetResult.ApiError -> _toast.emit(r.msg)
                is NetResult.NetError -> _toast.emit("网络异常")
                is NetResult.UnknownError -> _toast.emit("出了点问题")
            }
        }
    }
}

每次写 when 还是有点啰嗦,而且 ApiErrorNetError 给用户的提示其实经常一样。可以加个扩展收一下:

kotlin 复制代码
fun <T> NetResult<T>.onResult(
    onOk: (T) -> Unit,
    onErr: (String) -> Unit
) {
    when (this) {
        is NetResult.Success -> onOk(data)
        is NetResult.ApiError -> onErr(msg)
        is NetResult.NetError -> onErr("网络异常,检查一下网络")
        is NetResult.UnknownError -> onErr("出了点问题")
    }
}

调用就清爽多了:

kotlin 复制代码
safeCall { api.getUser(id) }.onResult(
    onOk = { _user.value = it.data },
    onErr = { _toast.emit(it) }
)

多 BaseUrl 场景

一个项目调多个域名很常见------主站一个、文件上传一个、IM 一个。给每个域名单独建 Retrofit 实例:

kotlin 复制代码
object ApiService {
    private val okHttp = ... // 复用同一个 OkHttpClient

    val main = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(okHttp)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val upload = 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()

Flow 适配

如果你的项目全面上了 Flow,再包一层也很简单:

kotlin 复制代码
fun <T> netFlow(block: suspend () -> T): Flow<NetResult<T>> = flow {
    emit(safeCall(block))
}.flowOn(Dispatchers.IO)

底层怎么跑的

你写了一个 interface,没写实现类,Retrofit 怎么就能发出去请求------这部分是靠 Java 的动态代理

大致流程:

  1. retrofit.create(UserApi::class.java) 调了 Proxy.newProxyInstance(),给你返回一个代理对象,不是编译期生成的类。
  2. 你调 userApi.getUser(123),这个调用被 InvocationHandler.invoke() 拦下来。
  3. Retrofit 拿到被调用的 Method 对象,解析上面的注解------@GET 拿到方法和路径,@Path 拿到参数映射关系。
  4. 注解信息 + 参数 + 返回类型,打包成一个 ServiceMethod,缓存起来,同一个方法下次调用直接复用。
  5. ServiceMethod 构建出一个 okhttp3.Call,然后看返回类型------你是 Call<T> 就直接返回 Call,你是 suspend 就交给 SuspendCallAdapterFactory 转成协程挂起,等 OkHttp 返回结果后恢复。
  6. OkHttp 返回 ResponseBodyConverter 反序列化成你声明的类型。

有个点值得注意:注解解析和 ServiceMethod 构建发生在第一次调用时 ,不是 retrofit.create() 的时候。所以接口里注解写错了,编译期不报错,得跑到那个方法才会炸。

Converter 选型

Gson 够用,但如果你用 Kotlin,Moshi 对 null-safety 和默认参数的支持要舒服得多。Kotlin Serialization 的话编译期生成序列化代码,性能更好,但需要加 @Serializable 注解和 Kotlin 插件。

kotlin 复制代码
// Moshi
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi))

// Kotlin Serialization
val json = Json { ignoreUnknownKeys = true }
Retrofit.Builder()
    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

自定义 Converter 也很直接------比如有些接口返回纯文本:

kotlin 复制代码
class StringConverterFactory : Converter.Factory() {
    override fun responseBodyConverter(
        type: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *>? {
        return if (type == String::class.java) {
            Converter<ResponseBody, String> { it.string() }
        } else null
    }
}

// 注册------特殊类型放前面,Gson 放最后
Retrofit.Builder()
    .addConverterFactory(StringConverterFactory())
    .addConverterFactory(GsonConverterFactory.create())

Converter 是按注册顺序尝试的,谁的 responseBodyConverter 先返回非 null,谁就接这个活。把最通用的(Gson)放最后。

CallAdapter:协程怎么接上的

Retrofit 内置了 SuspendCallAdapterFactory,它在 retrofit.create() 的时候就被默认加进去了。当你的方法用 suspend 声明时,Retrofit 检查返回类型,匹配到这个 Factory,把调用转成一个 OkHttp Call,然后 call.await() 让协程挂起,等结果回来后恢复。

RxJava 用户加 RxJava3CallAdapterFactory 就行,接口直接返回 Single<T> / Observable<T>

拦截器:不止是加 Token

OkHttp 的 Interceptor 是 Retrofit 最实用的扩展点:

Token 自动刷新(401 时换 Token 后重试):

kotlin 复制代码
class TokenInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var resp = chain.proceed(chain.request())
        if (resp.code == 401) {
            synchronized(this) {
                val newToken = refreshToken() // 同步刷新
                resp.close()
                val req = chain.request().newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
                resp = chain.proceed(req)
            }
        }
        return resp
    }
}

请求重试(指数退避):

kotlin 复制代码
class RetryInterceptor(private val maxRetry: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var resp: Response
        var count = 0
        var delay = 1000L
        do {
            resp = chain.proceed(chain.request())
            if (resp.isSuccessful || count >= maxRetry) break
            resp.close()
            Thread.sleep(delay)
            delay *= 2
            count++
        } while (count <= maxRetry)
        return resp
    }
}

运行时切 BaseUrl

比如 AB 测试切不同环境,或者多租户不同域名。Retrofit 原生不支持运行时换 baseUrl,可以通过拦截器 hack:

kotlin 复制代码
class DynamicUrlInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var req = chain.request()
        val newUrl = req.url.toString().replace(
            "https://api.example.com/",
            EnvManager.currentBaseUrl
        )
        req = req.newBuilder().url(newUrl).build()
        return chain.proceed(req)
    }
}

不过我自己的项目一般用 buildFlavor + buildConfigField 编译期切环境,比运行时稳。

踩过的坑

baseUrl 必须以 "/" 结尾

baseUrl("https://api.example.com") 不会报错,跑起来就炸。Retrofit 对这个是强校验的,少个 /,解析路径的时候就拼不对。养成肌肉记忆,最后一定带 /

@Path 和 @Query 的名字跟接口定义对不上

没有编译期检查,全靠眼睛对齐。写错一个字符,请求路径就错了,排查老半天。我的习惯是把路径参数定义成常量放一个地方:

kotlin 复制代码
object Keys {
    const val USER_ID = "userId"
}

@GET("user/{userId}")
suspend fun getUser(@Path(Keys.USER_ID) id: Long): User

主线程调 execute()

Call.execute() 同步阻塞,主线程调直接 ANR。如果你拿到的是 Call<T> 对象,用 enqueue() 异步,或者直接 suspend 让 Retrofit 处理。

Gson 把 null 赋给非空字段

Kotlin 里 val data: T 声明非空,但 Gson 不管这套,接口返回 "data": null 它照塞不误,然后 JsonSyntaxException 炸得莫名其妙。要么实体类全用可空类型,要么换 Moshi 或 Kotlin Serialization。

协程取消后请求未必停

Retrofit 的 suspend 函数在协程取消时会自动 cancel 底层 OkHttp Call,这点没问题。但如果你在 suspend 里手动 withContext(Dispatchers.IO) 又不检查 isActive,就有可能取消了还在跑。直接用 Retrofit 的 suspend 就行,别自己在线程池里套娃。

OkHttp 自动重试打到不同 IP

默认 retryOnConnectionFailure 是 true,失败了会自动换 IP 重试。如果你的业务对幂等性敏感、或者服务器多节点状态不一致,关掉:

kotlin 复制代码
OkHttpClient.Builder().retryOnConnectionFailure(false)

HttpLoggingInterceptor 把 Token 打日志里

Level.BODY 调试时很方便,但 Token、密码都在 Logcat 明文可见。Release 包务必关掉或者自己写个脱敏版:

kotlin 复制代码
level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE

Converter 注册顺序搞反

同时用 ScalarsConverterFactoryGsonConverterFactory,如果 Scalars 的 String 处理先匹配了,后面的 Gson 压根没机会。特殊类型放前面,通用放最后。

没配混淆规则

Retrofit 和 Gson 重度依赖反射和注解,不配 ProGuard 线上直接 ClassNotFoundException / 字段名对不上。最小规则:

kotlin 复制代码
-keepattributes Signature
-keepattributes *Annotation*
-keep class retrofit2.** { *; }
-keepclasseswithmembers class * {
    @retrofit2.http.* <methods>;
}
-keep class com.your.package.model.** { *; }

实体类一定记得 keep,不然字段名被混淆了 Gson 就找不到。

Retrofit 跟 Ktor 怎么选

顺带聊两句 Ktor Client。它也是 Kotlin 写的,协程原生,支持多平台(Android / iOS / Desktop),不用注解、纯 DSL 构建请求。轻量、灵活,但扩展机制和生态跟 Retrofit 比还差一截------OkHttp 那一整套拦截器、缓存、连接池的成熟度,Ktor 的引擎层短期内追不上。

我的建议:纯 Kotlin Multiplatform 项目可以上 Ktor;常规 Android 项目 Retrofit + OkHttp 还是最稳的选择,踩坑成本最低。

最后

用 Retrofit 这么多年,感受就是它不折腾人。你写好 interface、配好 OkHttp 和 Converter,剩下交给它跑,大部分时候你甚至意识不到它在那。

上手顺序建议:先把 GET / POST 跑通 → 把 OkHttp 拦截器配好(Token、日志、超时)→ 用 safeCall 收拢异常 → 有需要再加自定义 Converter 或 CallAdapter。别一上来就往里塞花活,够用就行。

几个容易忘的:baseUrl 末尾加 /、POST JSON 用 @Body 别写 @Field、suspend 函数别手动切线程、混淆别忘了 keep。做到这几点,Retrofit 一般不会给你找麻烦。

相关推荐
pyz66617 天前
Retrofit 源码分析
android·retrofit
我命由我123451 个月前
Retrofit - URL 格式错误问题、支持 HTTP 与 HTTPS
java·http·https·java-ee·android studio·android-studio·retrofit
赏金术士1 个月前
Retrofit + Kotlin 协程(Android 实战教程)
android·kotlin·retrofit
帅次2 个月前
链路到端上:HTTPS 之后安全题还在考什么
android·okhttp·glide·zygote·retrofit
明天就是Friday2 个月前
Android实战项目② Retrofit+Hilt开发天气预报App 完整源码详解
android·retrofit
普通网友2 个月前
Android开发:使用Kotlin+协程+自定义注解+Retrofit的网络框架
android·kotlin·retrofit
XiaoLeisj2 个月前
Android 短视频项目首页开发实战:从广场页广告轮播与网格列表,到发现页分类、播单与话题广场的数据驱动实现
android·okhttp·mvvm·recyclerview·retrofit·databinding·xbanner 轮播
番茄去哪了3 个月前
Retrofit框架调用第三方api
java·服务器·retrofit
常利兵3 个月前
从“新老交锋”看Retrofit与Ktor
retrofit