3、Retrofit最佳实践:Retrofit与OkHttp的关系、接口定义规范、Converter与Adapter选择、动态URL与Header处理、错误处理与异常封装
这一章我们来聊聊 Retrofit。说实话,Retrofit 在 Android 网络请求中的地位,就像 RecyclerView 在列表中的地位一样——几乎成了标配。但很多人用了很久,其实并没有真正用好它。
我见过不少项目,Retrofit 接口定义得乱七八糟,Converter 选错导致序列化性能低下,异常处理全靠 try-catch 一把梭。嗯,今天我们就把这些坑一个个填平。
3.1 Retrofit与OkHttp的关系
很多人搞不清这两者的关系。说白了,Retrofit 是皮,OkHttp 是骨。
Retrofit 本质上是一个 REST 客户端框架,它负责把 Java 接口转换成 HTTP 请求。但真正干活的是 OkHttp——建立连接、管理连接池、处理重定向、读写数据,这些都是 OkHttp 的功劳。
我习惯把 Retrofit 比作「翻译官」,把 OkHttp 比作「快递员」。翻译官把你要说的话(接口定义)翻译成快递单(HTTP 请求),快递员负责把包裹送到目的地。
核心要点:Retrofit 默认使用 OkHttp 作为底层网络层。你可以通过 OkHttpClient.Builder 自定义连接超时、拦截器、缓存等行为,然后传给 Retrofit.Builder。
// 自定义 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.build();
// 传给 Retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client) // 这里注入 OkHttpClient
.addConverterFactory(GsonConverterFactory.create())
.build();
我在项目中遇到过一个问题:团队里有人直接在 Retrofit.Builder 里设置超时,结果发现根本不生效。为什么?因为超时是 OkHttp 的职责,Retrofit 只是透传。所以记住:网络配置找 OkHttp,接口定义找 Retrofit。
3.2 接口定义规范
接口定义看似简单,但写得好不好,直接影响到后续的维护成本。我总结了几个原则:
3.2.1 命名规范
接口名用名词复数,方法名用 HTTP 方法对应的动词。别问我为什么,这是 RESTful 的最佳实践。
// 好的命名
public interface UserService {
@GET("users")
Call<List<User>> getUsers();
@POST("users")
Call<User> createUser(@Body User user);
}
// 不好的命名
public interface Api {
@GET("getUserList")
Call<List<User>> getUserList(); // URL 里不要带动词
}
3.2.2 路径参数与查询参数
路径参数用 @Path,查询参数用 @Query。我见过有人把所有参数都塞进 @QueryMap,结果 URL 变得又长又丑。
@GET("users/{id}")
Call<User> getUser(@Path("id") int userId, @Query("fields") String fields);
3.2.3 接口粒度控制
一个 Service 接口不要放太多方法。我习惯按业务模块拆分:UserService、OrderService、ProductService。每个接口不超过 10 个方法。你想想看,一个接口上百个方法,维护起来得多痛苦?
个人建议:每个 Service 接口对应一个业务模块,方法数量控制在 5-8 个。如果超过 10 个,考虑拆分。
3.3 Converter与Adapter选择
Converter 负责序列化和反序列化,Adapter 负责将 Call 转换成其他类型(如 RxJava 的 Observable)。选错了,性能会大打折扣。
3.3.1 Converter 的选择
| Converter | 适用场景 | 性能 |
|---|---|---|
| GsonConverterFactory | 通用场景,兼容性好 | 中等 |
| MoshiConverterFactory | 对性能有要求,且需要严格 JSON 解析 | 较高 |
| Kotlinx Serialization | 纯 Kotlin 项目 | 高 |
| JacksonConverterFactory | 需要复杂配置(如多态、自定义序列化) | 中等 |
我个人习惯用 Moshi。为什么?因为 Gson 在解析复杂泛型时容易出问题,而且 Moshi 的 Kotlin 支持更好。我曾经在一个项目中用 Gson 解析 List<List<User>>,结果反序列化出来全是 LinkedTreeMap,排查了半天才发现是泛型擦除的问题。换成 Moshi 后,问题迎刃而解。
3.3.2 Adapter 的选择
如果你用 RxJava,那就加 RxJava2CallAdapterFactory。如果你用 Kotlin 协程,Retrofit 2.6+ 已经原生支持 suspend 函数,不需要额外 Adapter。
// RxJava 方式
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.build();
// Kotlin 协程方式(不需要 Adapter)
interface UserService {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): User
}
注意:如果你同时使用多个 Adapter,注意顺序。Retrofit 会按添加顺序查找匹配的 Adapter,第一个匹配的会被使用。
3.4 动态URL与Header处理
实际项目中,URL 和 Header 经常需要动态变化。比如根据用户身份切换不同的 API 地址,或者动态添加 Token。
3.4.1 动态URL
用 @Url 注解可以覆盖 baseUrl。我遇到过需要从服务器动态获取图片地址的场景,这时候 @Url 就派上用场了。
@GET
Call<ResponseBody> downloadFile(@Url String fileUrl);
// 调用时传入完整 URL
apiService.downloadFile("https://cdn.example.com/images/logo.png");
3.4.2 动态Header
有两种方式:@Header 和 @HeaderMap。单个 Header 用 @Header,多个用 @HeaderMap。
// 单个动态 Header
@GET("users")
Call<List<User>> getUsers(@Header("Authorization") String token);
// 多个动态 Header
@GET("users")
Call<List<User>> getUsers(@HeaderMap Map<String, String> headers);
但说实话,我更推荐用 OkHttp 的拦截器来处理全局 Header,比如 Token。这样每个接口不用重复传参。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getToken())
.header("App-Version", BuildConfig.VERSION_NAME)
.build();
return chain.proceed(request);
})
.build();
3.5 错误处理与异常封装
这是最容易被忽视的部分。很多人的代码里,网络请求的异常处理就是一行 e.printStackTrace()。嗯,这样写确实省事,但线上出了问题你根本不知道发生了什么。
3.5.1 常见的异常类型
| 异常类型 | 原因 | 处理方式 |
|---|---|---|
| SocketTimeoutException | 连接超时或读取超时 | 提示用户检查网络,自动重试 |
| UnknownHostException | DNS 解析失败 | 提示网络不可用 |
| HttpException | 服务器返回非 2xx 状态码 | 根据状态码做差异化处理 |
| JsonSyntaxException | JSON 解析失败 | 记录日志,提示数据异常 |
3.5.2 统一异常封装
我习惯封装一个 ApiResult 类,把成功和失败统一处理。
public class ApiResult<T> {
private int code;
private String message;
private T data;
public boolean isSuccess() {
return code == 200;
}
// getter/setter 省略
}
// 在 Repository 层统一处理
public class UserRepository {
public void getUser(int id, Callback<ApiResult<User>> callback) {
apiService.getUser(id).enqueue(new Callback<ApiResult<User>>() {
@Override
public void onResponse(Call<ApiResult<User>> call, Response<ApiResult<User>> response) {
if (response.isSuccessful()) {
ApiResult<User> result = response.body();
if (result != null && result.isSuccess()) {
callback.onSuccess(result.getData());
} else {
callback.onError(new ApiException(result.getCode(), result.getMessage()));
}
} else {
callback.onError(new HttpException(response.code()));
}
}
@Override
public void onFailure(Call<ApiResult<User>> call, Throwable t) {
callback.onError(new NetworkException(t));
}
});
}
}
避坑指南:我曾经在 onFailure 里直接弹 Toast,结果用户反馈「明明有网,为什么提示网络错误?」。后来排查发现,是 JSON 解析失败触发了 onFailure。所以记住:onFailure 不一定是网络问题,也可能是数据格式问题。
3.5.3 全局异常拦截器
用 OkHttp 的拦截器可以统一处理一些通用错误,比如 Token 过期自动刷新。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() == 401) {
// Token 过期,尝试刷新
String newToken = refreshToken();
if (newToken != null) {
// 重试请求
Request newRequest = request.newBuilder()
.header("Authorization", "Bearer " + newToken)
.build();
return chain.proceed(newRequest);
}
}
return response;
})
.build();
好了,这一章的内容就到这里。Retrofit 用好了,网络请求这块基本就稳了。下一章我们聊聊缓存策略,这可是性能优化的重头戏。