版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/

Retrofit 简介

Retrofit 是 Square 公司开发的一个 类型安全的 HTTP 网络请求库,广泛应用于 Android 开发中。它极大地简化了 REST API 的调用方式,让网络请求就像调用本地接口一样简单。

基于 OkHttp 实现底层通信,提供了:

  • 注解方式定义 API 接口

  • 自动将 JSON/XML 响应解析为 Java/Kotlin 对象

  • 支持多种 Converter(如 Gson、Moshi、Protobuf)

  • 支持协程、RxJava、LiveData 等异步处理方式

与 OkHttp 关系:

  • Retrofit 是高级封装,负责将请求/响应转换为 Java 对象

  • OkHttp 是底层网络传输库,处理连接、缓存、拦截器等

  • Retrofit 默认使用 OkHttp,可以无缝集成 OkHttp 的功能(如添加 Token 拦截器)

开源地址:https://github.com/square/retrofit

集成 Retrofit

在 app 的 build.gradle.kts 添加如下依赖:

// Retrofit 核心库
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// JSON 转换器(使用 Gson)
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// OkHttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// OkHttp 日志拦截器(可选,用于调试)
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

然后点击 “Sync Now” 进行 Gradle 同步。

编辑 AndroidManifest.xml,添加访问网络权限:

<uses-permission android:name="android.permission.INTERNET" />

使用实例

1. 定义数据类

package com.cyrus.example.retrofit.model

data class User(
    val id: Int,
    val name: String,
    val email: String
)

2. Retrofit 接口定义(API Interface)

使用注解描述请求类型、路径、参数等:

package com.cyrus.example.retrofit.network

import com.cyrus.example.retrofit.model.User
import retrofit2.http.GET
import retrofit2.http.Path

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): User
}

3. Retrofit 实例构建

package com.cyrus.example.retrofit.network

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    private val client = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .build()

    val api: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

4. 发起请求

CoroutineScope(Dispatchers.IO).launch {
    try {
        val user = RetrofitClient.api.getUser(1)
        resultText = "用户1: ${user.name} (${user.email})"
    } catch (e: Exception) {
        resultText = "请求失败: ${e.message}"
    }
}

Retrofit 会自动将请求结果 JSON 响应解析为 Kotlin 对象

word/media/image1.png

日志输出如下:

2025-07-17 23:01:50.355  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  --> GET https://jsonplaceholder.typicode.com/users/1
2025-07-17 23:01:50.355  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  --> END GET
2025-07-17 23:01:52.416  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  <-- 200 https://jsonplaceholder.typicode.com/users/1 (2060ms)
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  date: Thu, 17 Jul 2025 15:01:52 GMT
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  content-type: application/json; charset=utf-8
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  access-control-allow-credentials: true
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  cache-control: max-age=43200
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  etag: W/"1fd-+2Y3G3w049iSZtw5t1mzSnunngE"
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  expires: -1
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  nel: {"report_to":"heroku-nel","response_headers":["Via"],"max_age":3600,"success_fraction":0.01,"failure_fraction":0.1}
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  pragma: no-cache
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  report-to: {"group":"heroku-nel","endpoints":[{"url":"https://nel.heroku.com/reports?s=YWQRu2P%2FvZPdRO%2BFKUJHT1Kv87wV6qY2%2BEllROkIyHs%3D\u0026sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d\u0026ts=1751617599"}],"max_age":3600}
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  reporting-endpoints: heroku-nel="https://nel.heroku.com/reports?s=YWQRu2P%2FvZPdRO%2BFKUJHT1Kv87wV6qY2%2BEllROkIyHs%3D&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&ts=1751617599"
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  server: cloudflare
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  vary: Origin, Accept-Encoding
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  via: 2.0 heroku-router
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-content-type-options: nosniff
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-powered-by: Express
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-ratelimit-limit: 1000
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-ratelimit-remaining: 999
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-ratelimit-reset: 1751617644
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  age: 21523
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  cf-cache-status: HIT
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  server-timing: cfCacheStatus;desc="HIT"
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  server-timing: cfEdge;dur=5,cfOrigin;dur=0
2025-07-17 23:01:52.420  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  cf-ray: 960a9df9ab308969-PDX
2025-07-17 23:01:52.420  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  alt-svc: h3=":443"; ma=86400
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  {
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "id": 1,
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "name": "Leanne Graham",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "username": "Bret",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "email": "Sincere@april.biz",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "address": {
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "street": "Kulas Light",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "suite": "Apt. 556",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "city": "Gwenborough",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "zipcode": "92998-3874",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "geo": {
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I        "lat": "-37.3159",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I        "lng": "81.1496"
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      }
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    },
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "phone": "1-770-736-8031 x56442",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "website": "hildegard.org",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "company": {
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "name": "Romaguera-Crona",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "catchPhrase": "Multi-layered client-server neural-net",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "bs": "harness real-time e-markets"
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    }
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  }
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  <-- END HTTP (509-byte body)

请求失败

word/media/image2.png

日志输出如下:

2025-07-17 23:23:59.994  3886-19788 okhttp.OkHttpClient     com.cyrus.example                    I  --> GET https://jsonplaceholder.typicode.com/users/2
2025-07-17 23:23:59.994  3886-19788 okhttp.OkHttpClient     com.cyrus.example                    I  --> END GET
2025-07-17 23:25:10.160  3886-19788 okhttp.OkHttpClient     com.cyrus.example                    I  <-- HTTP FAILED: java.net.SocketTimeoutException: failed to connect to jsonplaceholder.typicode.com/104.21.64.1 (port 443) from /192.168.0.101 (port 47128) after 10000ms

当请求出错时会抛出异常,现在是通过 try-catch 去捕获处理请求出错的情况,但这样做代码中会存在大量 try-catch。

如何更优雅的处理请求结果?

更优雅地处理请求结果

封装一个更优雅的 request 方法来实现以下功能:

  • 自动在 IO 线程中执行网络请求;

  • 自动在主线程中处理结果;

  • 支持链式调用;

  • 不再依赖外部 try-catch。

代码实现如下:

package com.cyrus.example.retrofit.network

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class RequestResult<T>(
    private val block: suspend () -> T
) {
    private var success: ((T) -> Unit)? = null
    private var failure: ((Throwable) -> Unit)? = null

    fun onSuccess(block: (T) -> Unit): RequestResult<T> {
        success = block
        return this
    }

    fun onFailure(block: (Throwable) -> Unit): RequestResult<T> {
        failure = block
        return this
    }

    fun launch(scope: CoroutineScope) {
        scope.launch(Dispatchers.IO) {
            try {
                val result = block()
                withContext(Dispatchers.Main) {
                    success?.invoke(result)
                }
            } catch (e: Throwable) {
                withContext(Dispatchers.Main) {
                    failure?.invoke(e)
                }
            }
        }
    }
}

fun <T> request(block: suspend () -> T): RequestResult<T> {
    return RequestResult(block)
}

调用方式如下:

request {
    RetrofitClient.api.getUser(2)
}.onSuccess {
    resultText = "用户2: ${it.name} (${it.email})"
}.onFailure {
    resultText = "❌ 请求失败:${it.message}"
}.launch(coroutineScope)

请求成功

word/media/image3.png

请求失败

word/media/image4.png

Retrofit 是如何实现类型转换的?

Retrofit 实现类型转换的核心在于其 Converter 转换器机制,它可以将 HTTP 请求的 body 或响应的 body 与 Java/Kotlin 对象之间互相转换。

1. 接口定义与创建代理

接口方法定义

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): User
}

创建接口的 Service 实例。

retrofit.create(ApiService::class.java)

Retrofit 动态代理解析注解,构建一个 ServiceMethod,封装了请求构建器、参数处理器、响应解析器等。

Retrofit.create(MyApi.class)
    └──-> validateServiceInterface(MyApi.class) // 检查传入的是接口、没有泛型参数等
    └──-> Proxy.newProxyInstance(...) // 创建接口代理
         └──-> ServiceMethod.loadServiceMethod(method) // 缓存 + 构建 ServiceMethod
              └──-> ServiceMethod.parseAnnotations(retrofit, method)  // 根据方法注解生成实际的 ServiceMethod 实例,它是 Retrofit 中对“接口方法”的抽象表示,封装了请求构建、响应转换等逻辑。
                   └──-> RequestFactory.parseAnnotations(...) // 解析方法和参数注解,构造请求路径、Query、Body、Header 等,生成一个 RequestFactory,能用于构造 okhttp3.Request。
                       └── new Builder(...).build()
                         └── parseParameterAnnotations(...):
                              for each parameter
                              └── parseParameterAnnotation(annotation, ...)
                                   ↳ if (annotation is @Body)
                                       └── retrofit.requestBodyConverter(type, parameterAnnotations, methodAnnotations)
                                            ↳ 遍历 List<Converter.Factory> converterFactories
                                            ↳ 返回 Converter<T, RequestBody>
                   └──-> HttpServiceMethod.parseAnnotations(...) // 生成具体类型的 ServiceMethod 子类,用于实际发起请求并处理返回值。
                             └──-> 调用 CallAdapter 来适配返回值
                             └──-> 调用 Converter 将响应体 ResponseBody 转为目标类型

https://github.com/square/retrofit/blob/8c8305574d1ff1010b701962cd9e392cb0663861/retrofit/src/main/java/retrofit2/Retrofit.java#L182

Retrofit 通过 Proxy.newProxyInstance 创建接口的动态代理。每个接口方法调用都会触发 invoke(…)

return Proxy.newProxyInstance(
    service.getClassLoader(),
    new Class<?>[] {service},
    new InvocationHandler() {
        public Object invoke(Object proxy, Method method, Object[] args) {
            ServiceMethod<?> serviceMethod = loadServiceMethod(method);
            return serviceMethod.invoke(args); // 触发网络请求
        }
    }
);

2. Retrofit 请求 → 响应 → responseConverter

API 接口函数(如 suspend fun getUser())
       ↓
Retrofit.create(...) → 动态代理 → invoke()
       ↓
ServiceMethod.invoke(args)
       ↓
HttpServiceMethod.adapt(Call<ResponseT>, args)
       ↓
CallAdapter.adapt(...) → Kotlin 协程适配器
       ↓
OkHttpCall.enqueue(...) 异步请求
       ↓
OkHttp 拦截器链发起真实网络请求
       ↓
网络响应返回(Response)
       ↓
responseConverter.convert(ResponseBody) → JSON 解析成对象
       ↓
回到协程 → Kotlin 数据类对象返回

当有多个转换器 Retrofit 如何决定使用哪个?

数据模型与 Converter 之间是如何关联的?当有多个转换器 Retrofit 如何决定使用哪个?

你可以在注册 Converter 时同时添加 Gson 和 Protobuf,比如:

Retrofit.Builder()
    .baseUrl("https://your.api/")
    .addConverterFactory(ProtobufConverterFactory.create()) // 优先匹配 protobuf
    .addConverterFactory(GsonConverterFactory.create()) // fallback 到 Gson
    .build()

这样,如果你的接口参数/返回值是 MessageLite 类型(protobuf 的基类),就会走 ProtobufConverterFactory;否则 fallback 到 Gson。

如果多个 Converter 都能处理某类型怎么办?

Retrofit 的行为是:只使用最先注册的那个可以处理的 Converter。

Converter 与类型的关联是如何发生的?

  • Retrofit 持有一组 Converter.Factory 列表 converterFactories

  • 调用 responseBodyConverter(…) 方法遍历所有 factory

  • 哪个 factory 返回了 非 null 的 Converter,就代表这个 factory “能处理”这个类型,直接使用这个 Converter

  • 第一个非 null 的 converter 被使用,其余跳过

/**
 * Returns a {@link Converter} for {@link ResponseBody} to {@code type} from the available
 * {@linkplain #converterFactories() factories}.
 */
public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
  return nextResponseBodyConverter(null, type, annotations);
}

/**
 * 实际执行查找匹配 converter 的方法。
 *
 * @param skipPast 从哪个 Converter.Factory 之后开始查找(通常用于链式调用时跳过之前已经尝试过的)
 * @param type 数据模型的类型,例如 User::class.java、List<User>::class.java 等
 * @param annotations 方法或参数上的注解,可用于影响选择(如 @Streaming)
 */
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
    @Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {

  // 检查参数不为空
  Objects.requireNonNull(type, "type == null");
  Objects.requireNonNull(annotations, "annotations == null");

  // 找出从哪个 Factory 开始查找(如 skipPast = null,则从 0 开始)
  int start = converterFactories.indexOf(skipPast) + 1;

  // 遍历所有注册的 Converter.Factory
  for (int i = start, count = converterFactories.size(); i < count; i++) {
    // 调用工厂方法尝试获取能处理该类型的 Converter
    Converter<ResponseBody, ?> converter =
        converterFactories.get(i).responseBodyConverter(type, annotations, this);

    if (converter != null) {
      //noinspection unchecked
      return (Converter<ResponseBody, T>) converter;
    }
  }

  // 如果没有任何一个工厂返回非空 converter,说明 Retrofit 无法处理这个类型,抛出异常
  StringBuilder builder =
      new StringBuilder("Could not locate ResponseBody converter for ")
          .append(type)
          .append(".\n");

  // 打印哪些工厂被跳过
  if (skipPast != null) {
    builder.append("  Skipped:");
    for (int i = 0; i < start; i++) {
      builder.append("\n   * ").append(converterFactories.get(i).getClass().getName());
    }
    builder.append('\n');
  }

  // 打印尝试过的工厂
  builder.append("  Tried:");
  for (int i = start, count = converterFactories.size(); i < count; i++) {
    builder.append("\n   * ").append(converterFactories.get(i).getClass().getName());
  }

  throw new IllegalArgumentException(builder.toString());
}

/**
 * 用于将数据类型转换为字符串(如 @Query("name") 参数),匹配逻辑同样遍历 converterFactories。
 */
public <T> Converter<T, String> stringConverter(Type type, Annotation[] annotations) {
  Objects.requireNonNull(type, "type == null");
  Objects.requireNonNull(annotations, "annotations == null");

  for (int i = 0, count = converterFactories.size(); i < count; i++) {
    Converter<?, String> converter =
        converterFactories.get(i).stringConverter(type, annotations, this);
    if (converter != null) {
      //noinspection unchecked
      return (Converter<T, String>) converter;
    }
  }

  // 没找到匹配,退而求其次使用默认的 toString 转换器
  //noinspection unchecked
  return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
}

https://github.com/square/retrofit/blob/c2ba413943b2b22e5691c7d89d11e1748d70e261/retrofit/src/main/java/retrofit2/Retrofit.java#L401

GsonConverterFactory

GsonConverterFactory 是 Retrofit 中基于 Gson 实现的一个 Converter.Factory。它负责为 Retrofit 提供序列化(Java/Kotlin → JSON)和反序列化(JSON → Java/Kotlin)的能力。

GsonConverterFactory 能够处理几乎所有类型,因为它依赖 Gson 的泛型适配能力:Gson 在运行时可以通过类型推导构造适当的 TypeAdapter,而不是硬编码某个具体的类型。

override fun responseBodyConverter(
    type: Type,
    annotations: Array<Annotation>,
    retrofit: Retrofit
): Converter<ResponseBody, *>? {
    val adapter: TypeAdapter<*> = gson.getAdapter(TypeToken.get(type))
    return GsonResponseBodyConverter(gson, adapter)
}

gson.getAdapter(TypeToken.get(type)) 它会根据 type 生成对应的 TypeAdapter<T>,并交给 GsonResponseBodyConverter 使用。

所以,一般 GsonConverterFactory 放最后兜底。

如何自定义转换器?

如果某个接口返回的是纯文本(如 text/plain),并且你希望直接以 String 形式接收,可以通过自定义 Converter.Factory 来实现对特定类型(如 String.class)的支持。

示例:自定义 StringResponseConverterFactory

package com.cyrus.example.retrofit.network

import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type

class StringResponseConverterFactory : Converter.Factory() {

    override fun responseBodyConverter(
        type: Type, annotations: Array<Annotation>, retrofit: Retrofit
    ): Converter<ResponseBody, *>? {
        // 仅处理 String 类型的响应
        return if (type == String::class.java) {
            Converter<ResponseBody, String> { body -> body.string() }
        } else {
            null
        }
    }
}

当你初始化 Retrofit 时,将该工厂放在 GsonConverterFactory 之前:

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

接口定义示例:

package com.cyrus.example.retrofit.network

import retrofit2.http.GET

interface ApiService {
    
    @GET("users/1")
    suspend fun getPlainText(): String
}

接口调用示例:

request {
    RetrofitClient.api.getPlainText()
}.onSuccess {
    resultText = it
}.onFailure {
    resultText = "❌ 请求失败:${it.message}"
}.launch(coroutineScope)

效果如下:

word/media/image5.png

完整源码

开源地址:https://github.com/CYRUS-STUDIO/AndroidExample