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 还是有点啰嗦,而且 ApiError 和 NetError 给用户的提示其实经常一样。可以加个扩展收一下:
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 的动态代理。
大致流程:
retrofit.create(UserApi::class.java)调了Proxy.newProxyInstance(),给你返回一个代理对象,不是编译期生成的类。- 你调
userApi.getUser(123),这个调用被InvocationHandler.invoke()拦下来。 - Retrofit 拿到被调用的
Method对象,解析上面的注解------@GET拿到方法和路径,@Path拿到参数映射关系。 - 注解信息 + 参数 + 返回类型,打包成一个
ServiceMethod,缓存起来,同一个方法下次调用直接复用。 ServiceMethod构建出一个okhttp3.Call,然后看返回类型------你是Call<T>就直接返回 Call,你是suspend就交给SuspendCallAdapterFactory转成协程挂起,等 OkHttp 返回结果后恢复。- OkHttp 返回
ResponseBody,Converter反序列化成你声明的类型。
有个点值得注意:注解解析和 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 注册顺序搞反
同时用 ScalarsConverterFactory 和 GsonConverterFactory,如果 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 一般不会给你找麻烦。