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 用好了,网络请求这块基本就稳了。下一章我们聊聊缓存策略,这可是性能优化的重头戏。