Retrofit + RxAndroid 实践总结

article/2025/9/29 22:24:28

在接入 Retrofit + RxAndroid 之前,项目代码中主要存在如下问题:

  1. 服务器 API 的定义方式不一致,有的集中定义,有的定义在业务代码中,没有分类不便于维护。
  2. Request / Response / API 三者没有对应关系(Request 参数使用 Map 传递,Response 返回 JSON 数据)
  3. 每次都需要传递 access_token 给需要验证登录的 API
  4. Response 中错误信息的数据结构不一致,错误处理不统一

引入 Retrofit + RxAndroid 后,以上问题都会迎刃而解。

定义基类

首先定义一个 BaseResponse,所有的 Response 都要继承自它。

Response

@Keep
public class BaseResponse {public static final int CODE_SUCCESS = 0;public String msg;public int code;@SerializedName("error_response")public ErrorResponse errorResponse;public static final class ErrorResponse {public String msg;public int code;}
}

BaseResponse 的主要作用是统一了错误信息的格式,同时为后面统一错误处理打好基础。

ErrorResponseException

为了统一请求错误返回错误,我们定义了一个继承自 IOException 的子类 ErrorResponseException

public class ErrorResponseException extends IOException {public ErrorResponseException() {super();}public ErrorResponseException(String detailMessage) {super(detailMessage);}
}

定义 Service Method

public interface TradesService {@GET("kdt.tradecategories/1.0.0/get")Observable<Response<CategoryResponse>> tradeCategories();@FormUrlEncoded@GET("kdt.trade/1.0.0/get")Observable<Response<TradeItemResponse>> tradeDetail(@Field("tid") String tid);
}

其中 CategoryResponseTradeItemResponse 全部继承自 BaseResponse

泛型 Response 由 Retrofit 提供,定义了三个成员变量:

private final okhttp3.Response rawResponse;
private final T body;
private final ResponseBody errorBody;

可以看出,Response 是对 okhttp3.Response 的封装,body 是一个 BaseResponse 实例。

因为 Response 只会根据 code 值判断请求是否成功,而不会判断 body 的内容是否出错,所以我们把 Response 中的错误信息称作请求错误,把 body 中的错误信息称作返回错误

既然 Response 包含了 BaseResponse(即 body),那么我们就可以对两种错误(请求错误、返回错误)进行统一处理。

统一错误处理

Service Method 的返回值类型是 Observable<Response<? extends BaseResponse>,实际上业务方想要的是 Observable<? extends BaseResponse>,那么我们就定义一个 Transformer 来转换这两个 Observable

转换过程其实就是一个错误处理的过程,因为我们要从 Response 中把 BaseResponse 剥离出来,如果 Response 或者 BaseResponse 中含有错误信息则意味着转换失败,直接抛出我们已经定义好的 BaseErrorResponse,回调 SubscriberonError 方法。

public class ErrorCheckerTransformer<T extends Response<R>, R extends BaseResponse>implements Observable.Transformer<T, R> {public static final String DEFAULT_ERROR_MESSAGE = "Oh, no";private Context mContext;public ErrorCheckerTransformer(final Context context) {mContext = context;}@Overridepublic Observable<R> call(Observable<T> observable) {return observable.map(new Func1<T, R>() {@Overridepublic R call(T t) {String msg = null;if (!t.isSuccessful() || t.body() == null) {msg = DEFAULT_ERROR_MESSAGE;} else if (t.body().errorResponse != null) {msg = t.body().errorResponse.msg;if (msg == null) {msg = DEFAULT_ERROR_MESSAGE;}} else if (t.body().code != BaseResponse.CODE_SUCCESS) {msg = t.body().msg;if (msg == null) {msg = DEFAULT_ERROR_MESSAGE;}}if (msg != null) {try {throw new ErrorResponseException(msg);} catch (ErrorResponseException e) {throw Exceptions.propagate(e);}}return t.body();}});}
}

当然,你也可以在这里判断是否需要唤起登录请求。

创建 Service Method

不同的 Service Method 可能对应着不同的网关,因此我们需要定义一个工厂为不同的网关生产 Service Method。

public class ServiceFactory {public static final String OLD_BASE_URL = "https://liangfeizc.com/gw/oauthentry/";public static final String NEW_BASE_URL = "https://liangfei.me/api/oauthentry/";public static <T> T createOldService(Class<T> serviceClazz) {return createOauthService(OLD_BASE_URL, serviceClazz);}public static <T> T createNewService(Class<T> serviceClazz) {return createOauthService(NEW_BASE_URL, serviceClazz);}public static <T> T createOauthService(String baseUrl, Class<T> serviceClazz) {OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor() {@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();HttpUrl url = request.url().newBuilder().addQueryParameter("access_token", UserInfo.getAccessToken()).build();request = request.newBuilder().url(url).build();return chain.proceed(request);}}).build();Retrofit retrofit = new Retrofit.Builder().client(client).baseUrl(baseUrl).addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJavaCallAdapterFactory.create()).build();return retrofit.create(serviceClazz);}
}

因为这两个网关都要求登录后才能访问,因此我们通过 OkHttpClient#addInterceptor 拦截 Request 之后加上了参数 access_token

线程模型

大多数情况下,我们都会在 io 线程发起 request,在主线程处理 response,所以我们定义了一个默认的线程模型:

public class SchedulerTransformer<T> implements Observable.Transformer<T, T> {@Overridepublic Observable<T> call(Observable<T> observable) {return observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());}public static <T> SchedulerTransformer<T> create() {return new SchedulerTransformer<>();}
}

为了方便使用,我们又定义了一个 DefaultTransformerSchedulerTransformerErrorCheckerTransformer 结合起来。

public class DefaultTransformer<T extends Response<R>, R extends BaseResponse>implements Observable.Transformer<T, R> {private Context mContext;public DefaultTransformer(final Context context) {mContext = context;}@Overridepublic Observable<R> call(Observable<T> observable) {return observable.compose(new SchedulerTransformer<T>()).compose(new ErrorCheckerTransformer<T, R>(mContext));}
}

Subscriber

为了进一步统一错误消息的展示方式,我们又对 Subscriber 进行了一层封装。

BaseSubscriber

public abstract class BaseSubscriber<T> extends Subscriber<T> {private Context mContext;public BaseSubscriber(Context context) {mContext = context;}public Context getContext() {return mContext;}
}

ToastSubscriber

以 Toast 形式展示错误消息。

public abstract class ToastSubscriber<T> extends BaseSubscriber<T> {public ToastSubscriber(Context context) {super(context);}@CallSuper@Overridepublic void onError(Throwable e) {ToastUtil.show(getContext(), e.getMessage());}
}

DialogSubscriber

以 Dialog 形式展示错误消息。

public abstract class DialogSubscriber<T> extends BaseSubscriber<T> {public DialogSubscriber(Context context) {super(context);}@CallSuper@Overridepublic void onError(Throwable e) {DialogUtil.showDialog(getContext(), e.getMessage(), "OK", true);}
}

如何使用

我们以获取 Category 为例来说明如何利用 Retrofit 和 RxAndroid 来改写现有模块。

1. 定义 CategoryResponse

CategoryResponse 必须继承自 BaseResponse,里面包含了错误信息的数据结构。

@Keep
public class CategoryResponse extends BaseResponse {public Response response;@Keeppublic static final class Response {public List<Category> categories;}
}

其中 Category 是具体的实体类型。

2. 定义 Service Method

public interface TradesService {@GET("kdt.tradecategories/1.0.0/get")Observable<Response<CategoryResponse>> tradeCategories();

注意点

  • TradesService 必须是一个 interface,而且不能继承其他 interface
  • tradeCategories 的返回值必须是 Observable<Response<? extends BaseResponse>> 类型。

3. 利用 ServiceFactory 创建一个 TradeService 实例

在适当的时机(Activity#onCreateFragment#onViewCreated 等)根据网关类型通过 ServiceFactory 创建一个 TradeService 实例。

mTradesService = ServiceFactory.createNewService(TradesService.class)

4. TradeService 获取数据

mTradesService.tradeCategories().compose(new DefaultTransformer<Response<CategoryResponse>, CategoryResponse>(getActivity())).map(new Func1<CategoryResponse, List<Category>>() {@Overridepublic List<Category> call(CategoryResponse response) {return response.response.categories;}}).flatMap(new Func1<List<Category>, Observable<Category>>() {@Overridepublic Observable<Category> call(List<Category> categories) {return Observable.from(categories);}}).subscribe(new ToastSubscriber<Category>() {@Overridepublic void onCompleted() {hideProgressBar();// business related code}@Overridepublic void onError(Throwable e) {super.onError(e);hideProgressBar();// business related code}@Overridepublic void onNext(Category category) {// business related code}});

注意:DefaultTransformer 包含了线程分配错误处理两部分功能,所以调用方只需要关心正确的数据就可以了。

测试

NetworkBehavior - 网络环境模拟

private void givenNetworkFailurePercentIs(int failurePercent) {mNetworkBehavior.setDelay(0, TimeUnit.MILLISECONDS);mNetworkBehavior.setVariancePercent(0);mNetworkBehavior.setFailurePercent(failurePercent);
}

TestSubscriber - 带断言的 Subscriber

private TestSubscriber<Response<CategoryResponse>> mTestSubscriberCategory = TestSubscriber.create()
subscriber.assertError(RuntimeException.class);
subscriber.assertNotCompleted();

MockRetrofit - 为 Retrofit 添加 Mock 数据(NetworkBehavior 等)

@Before
public void setUp() {Retrofit retrofit = new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJavaCallAdapterFactory.create()).baseUrl(ServiceFactory.CARMEN_BASE_URL).build();MockRetrofit mockRetrofit = new MockRetrofit.Builder(retrofit).networkBehavior(mNetworkBehavior).build();
}

BehaviorDelegate - Retrofit Service 的代理,用于产生 Mock 数据

BehaviorDelegate<TradesService> delegate = mockRetrofit.create(TradesService.class);
mTradesServiceMock = new TradesServiceMock(delegate);
public class TradesServiceMock implements TradesService {private final BehaviorDelegate<TradesService> mDelegate;public TradesServiceMock(BehaviorDelegate<TradesService> delegate) {mDelegate = delegate;}@Overridepublic Observable<Response<CategoryResponse>> tradeCategories() {return mDelegate.returningResponse("{\"error_response\": \"my god\"}").tradeCategories();}
}

总结

通过以上实践可以看出,Retrofit + RxAndroid 大大改善了代码的可维护性。

  1. 以 API 为中心,Request、Response、Method 一一对应,开发效率飙升
  2. 告别 Callback Hell,以同步方式写异步代码,让代码结构更清晰,更易于维护
  3. 基于事件,各种 Operator,四两拨千斤,尽情发挥你的想象力。

http://chatgpt.dhexx.cn/article/T5EjPOQd.shtml

相关文章

Rxjava3 RxAndroid

文章目录 Rxjava && Rxandroid引用方式 概念流程图代码示例ObservableObservable#subscribeOn(NonNull Scheduler scheduler)Observable#observeOn(NonNull Scheduler scheduler)ObservableSubscribeOnObservableObserveOn本文开头的代码示例等同于如下代码 Schedulers…

RxJava和RxAndroid学习记录

目录 1 概念和说明 1.1 响应式编程 1.2 RxJava 1.3 关于RxJava和RxAndroid 1.4 关于响应式编程和普通编程 2. 基本使用 2.1 基本元素关系图 2.2 代码示例&#xff1a; 2.3 关于subscribe&#xff08;&#xff09; 2.4 线程调度 2.4.1 线程调度 2.4.2 RxJava内置的常用…

RxAndroid的基础使用

作为一个android开发者&#xff0c;在开发应用的过程中避免不了异步这个问题。android系统为我们提供了Handler这个类帮助我们进行线程间的通信和切换&#xff0c;但是GitHub上也有很多其他非常优秀的开源框架来帮助我们进行异步处理&#xff0c;比如今天学习的RxAndroid。 简…

rxandroid 基础知识

概述 在Android 中, 使用 rxandroid , rxandroid和rxJava的关系是,rxandroid包 依赖rxJava包,在其功能上增加了一些Android特有功能,项目中如果不需要指定rxJava包的版本,只需引入rxandroid包即可,如果需要更改 rxandroid包中默认的rxJava包版本 , 在项目中引入指定的rxJava包即…

RxAndroid使用初探;简洁、优雅、高效

引言 RxAndroid是一个开发库、是一种代码风格、也是一种思维方式。 正如标题所言,RxAndroid的特点是简洁、优雅、高效,它的优点是多线程切换简单、数据变换容易、代码简洁可读性好、第三方支持丰富易于开发;缺点是学习成本较高、出错难以排查。 用途与优势 起源 RxAndroid…

RxAndroid的学习和研究

1.什么是RxAndroid RxAndroid的含义为响应式编程&#xff0c;Rx含义是响应式编程&#xff0c;其本质就是观察者模式&#xff0c;以观察者&#xff08;Observer&#xff09;和订阅者&#xff08;Subscriber&#xff09;为基础的异步响应方式。    Observables发出一系列事件&a…

linux基本功系列之dd命令实战

文章目录 前言&#x1f680;&#x1f680;&#x1f680;一. dd 命令介绍二. 语法格式及常用选项三. 参考案例3.1 创建指定大小的文件3.2 清空磁盘数据3.3 给磁盘做备份还原3.4 把光盘拷贝到root下3.5 内存不足的处理方法 四. 文中出现的概念解释swapon命令介绍4.2 /dev/zero 介…

Linux系统中dd命令用法详解

命令介绍&#xff1a; Linux dd 命令用于读取、转换并输出数据。dd 可从标准输入或文件中读取数据&#xff0c;根据指定的格式来转换数据&#xff0c;再输出到文件、设备或标准输出。 参数介绍 if 代表输入文件。如果不指定 if&#xff0c;默认就会从 stdin 中读取输入。of …

dd 命令详解

dd命令是Linux/Unix下的一个很常见的文件拷贝工具。 我们先列下dd命名的常用的参数&#xff0c;再详细分析&#xff1a; bsBYTES read and write up to BYTES bytes at a time cbsBYTES convert BYTES bytes at a time convCONVS convert the file as pe…

dd命令使用总结

dd命令介绍 dd是Linux下一个非常有用的命令&#xff0c;该命令用于读取、转换并输出数据&#xff1b;dd命令在Android shell下也支持使用。 语法格式&#xff1a; dd [option]dd指令选项详解 iffile&#xff1a;输入文件名&#xff0c;缺省为标准输入 offile&#xff1a;输…

dd命令相关整理

对于一个软件测试人员而言&#xff0c;工作开展前就是准备自己的测试环境&#xff0c;那么重装系统就是首当其冲的一个必备技能。最近因为手边工作环境没有windows的系统&#xff0c;所以没有条件利用软碟通这类刻录软件直接刻录启动盘。被逼无奈之下用命令来刻录&#xff0c;整…

Linux:shell 脚本 自动解压压缩文件tar.gz到指定目录

具体情境 Ubuntu16.04系统&#xff0c;将.tar.gz格式的文件从/home/myftp/upload/nuodongiot目录自动解压到/home/myftp/upload/backupcopy目录中&#xff0c;并将源目录/home/myftp/upload/nuodongiot中的文件移动至/home/myftp/upload/extarct目录中 该过程进行单个文件进行…

tar解压文件至指定目录,不包含原目录

1、tar解压文件至指定目录&#xff0c;不包含原目录 要解压的压缩包原目录结构如下 tar -zxf log.tar.gz --strip-components 1 -C /opt/new_test注&#xff1a; --strip-components 1 解压至下一级目录&#xff0c;若为2则解压至下下级目录 2、压缩只指定的目录&#xff0c…

linux gz解压 指定目,linux解压tar.gz到指定文件夹或目录

1. 前言 本文主要讲解如何解压tar.gz到指定文件夹或目录,tar是Linux系统上的一种打包与压缩工具。 2. linux解压tar文件使用案例 Linux下使用tar命令把当前目录下的zcwyou.tar.gz解压到指定的目录/123/abc/ ,前提要保证存在/123/abc/这个目录。 [root@zcwyou ~]# tar -zxvf zc…

Linux tar 命令 将归档内指定文件解压到指定目录

首先介绍一下 tar 命令&#xff1a; 用途&#xff1a;打包文件&#xff08;制作归档文件&#xff09;、释放归档文件 格式&#xff1a; tar [选项]... 归档文件名 源文件或目录 tar [选项]... 归档文件名 [-C 目标目录] 常用命令选项&#xff1a; -c 创建 .tar 格式…

20191004在LINUX下如何将tar压缩文件解压到指定的目录下

百度搜索&#xff1a;tar 解压缩到指定目录 https://zhidao.baidu.com/question/9844116.html 在LINUX下如何将tar压缩文件解压到指定的目录下 各位&#xff0c;请教一下在LINUX下如何将tar压缩文件解压到指定的目录下&#xff0c;直接用tar xvf 解压出来的是放在当前目录的&am…

关于linux打包以及解压到指定目录的简单操作demo

1.打包到指定目录 命令:tar zcvf /root/test99/a.tar.gz a.txt 1.1打包到当前目录 命令:tar -zcvf a.tar.gz a.txt 2.解压到指定目录 命令: tar -zxvf a.tar.gz -C /root/test99 2.2解压到当前目录 命令:tar -zxvf a.tar.gz 打zip包: 方法如下&#xf…

linux tar解压文件至指定目录,不包含原目录

1、tar解压文件至指定目录&#xff0c;不包含原目录 要解压的压缩包原目录结构如下 通过 --strip-components 1 参数 解压到指定目录或当前目录&#xff08;不含打包前原目录&#xff09; tar zxf log.tar.gz --strip-components 1 -C /opt/new_test注&#xff1a; --strip-co…

Linux拓展之产生随机数

在 Linux 中可以通过内置变量 RANDOM 来产生随机数&#xff0c;该变量会产生一个 [0, 32767] 范围内的随机整数。如下&#xff1a; echo $RANDOM如果要产生 [0-10] 之内的随机整数&#xff1a;echo $(( $RANDOM % 10 )) 如果要产生 [1-10] 之内的随机整数&#xff1a;echo $((…

Linux生成随机数

生成随机数的方法有7种 1.通过时间获取随机数 1&#xff09;date %s &#xff08;随机生成10位数字&#xff09; 用于获得时间戳。 如果用它做随机数&#xff0c;相同一秒的数据是一样的。在做循环处理&#xff0c;多线程里面基本不能满足要求了。 2&#xff09;date…