有赞 Android 客户端网络架构演进
Android 客户端网络请求是每一个应用都不可或缺的模块,其设计的好坏直接影响应用的性能和代码稳定性、扩展性。Android 网络请求最开始官方只提供了最基础的方法,开发者必须在此基础上进行二次封装,这样就要求开发者对 Http 请求协议、缓存、JSON 转换、错误处理以及线程切换等都比较熟悉,稳定性、可扩展性和可维护性都是比较大的挑战。
目前 Android 主流的网络请求都是基于 Square 公司开发的 OkHttp,该框架也得到了 Google 官方的认可,OkHttp 对网络请求做了大量的封装和优化,极大降低了开发者的使用成本,同时兼备稳定性和可扩展性。目前有赞 Android 客户端也是采用 OkHttp 进行网络请求,在 OkHttp 框架的基础上做了大量的封装和优化,减小业务逻辑与框架的耦合性的同时,也极大降低了业务方的使用成本。
1. 现在的网络请求
先以 Get 请求为例,代码如下:
private static final String TRADE_CATEGORIES = "kdt/tradecategories/get";
/**
* 获取订单类型
*
* @param context context
* @param callback TradeCategoryEntity callback
*/
public void requestTradeCategories(Context context,Map<String, String> params, BaseTaskCallback<List<TradeCategoryEntity>> callback) {
RequestApi api = createRequestTokenApi(TRADE_CATEGORIES);
if (null != params) {
api.addMultipleRequestParams(params);
}
api.setMethod(RequestApi.Method.GET);
api.setResponseKeys(GoodsTradeApi.ResponseKey.RESPONSE, "categories");
doRequest(context, api, true, callback, TaskErrorNoticeMode.NONE);
}
针对上述 Get 请求需要几点说明:
- 每个模块都需要一个单独的网络请求类,罗列该模块所有的业务请求方法
- 请求 url 需要在每个请求类中定义,如上文中的
TRADE_CATEGORIES
- 每次请求都需要调用
createRequestTokenApi
方法,完成 token 参数的封装 - 请求参数可以直接通过
addRequestParams
添加,也可以以 Map 的形式统一添加addMultipleRequestParams
- 请求方法通过
setMethod
方法设置 - 返回值可以通过
setResponseKeys
指定需要的具体 JSON 节点 - 请求结果通过
BaseTaskCallback
回调,返回值类型也可通过泛型方式指定 - 回调方法包括请求成功、请求失败以及请求时机等方法
以下是请求结果回调方法代码:
// 请求开始的回调方法
public void onBefore(RequestApi api) {
}
// 请求结束的回调方法
public void onAfter() {
}
// 请求成功的回调方法,data表示返回值,statusCode表示请求状态值
public void onResponse(T data, int statusCode){
}
// 网络请求失败的回调方法
public void onError(ErrorResponse errorResponse) {
}
// 请求的业务错误回调方法
public void onRequestError(ErrorResponse errorResponse) {
}
业务方只需要在不同的回调方法中处理不同的业务即可,比如说在onBefore
中显示进度条,在onAfter
中隐藏进度条,在onResponse
中获取返回值以及状态值,onRequestError
中获取错误类型,以及显示错误提示等等。
针对上述请求过程,可以发现还是有一些不是很合理的地方。
- 每次新建一个网络接口方法都需要设置请求 url、封装 token、设置请求方法等,造成了大量的重复代码
- 请求 url 和具体的请求方法分开定义,不够直观
- 如果请求参数比较多的时候,使用 Map 方式容易写错,而且在编译过程和自测阶段不会有错误提示
- 请求成功回调方法的线程切换还是需要借助 Handler 等方式实现
2. 改进后的网络请求框架
针对以上问题,我们引入 Square 公司开发的 Retrofit 和 ReactiveX 出品的 RxJava,以下是结合有赞业务后网络请求用法。
首先定义网络请求方法,Retrofit 是通过接口的方式完成请求方法定义的,代码如下:
public interface TradesService {
@GET("api.tradecategories/1.0.0/get")
Observable<Response<CategoryResponse>> tradeCategories();
@FormUrlEncoded
@GET("api.trade/1.0.0/get")
Observable<Response<TradeItemResponse>> tradeDetail(@Field("tid") String tid);
}
通过注解的方式指定请求类型、请求 url、请求参数,返回值是 RxJava 的Observable
对象,就可以进行一系列的链式操作,具体用法等一下会详细讲述。通过对比两种请求方法的写法,很显然,通过 Retrofit 定义请求方法更简洁清晰,下面我们来看一下如何通过 RxJava 实现错误信息的统一处理。
2.1 请求错误处理
首先定义一个BaseResponse
,所有的Response
都要继承自它。
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
定义了错误信息,后续所有的Response
都会继承BaseResponse
,而服务端返回的错误信息都会进行集中处理,处理逻辑代码如下:
// 下文的T:T extends Response<R>, R: R extends BaseResponse
public Observable<R> call(Observable<T> observable) {
return observable.map(new Func1<T, R>() {
@Override
public R call(T t) {
String msg = null;
if (!t.isSuccessful() || t.body() == null) {
msg = mDefaultErrorMsg;
} else if (t.body().code != BaseResponse.CODE_SUCCESS) {
msg = t.body().msg;
if (msg == null) {
msg = mDefaultErrorMsg;
}
tryLogin(t.body().code);
} else if (t.body().errorResponse != null) {
msg = t.body().errorResponse.msg;
if (msg == null) {
msg = mDefaultErrorMsg;
}
tryLogin(t.body().errorResponse.code);
}
if (msg != null) {
try {
throw new ErrorResponseException(msg);
} catch (ErrorResponseException e) {
throw Exceptions.propagate(e);
}
}
return t.body();
}
});
关于上面的代码有几点需要说明:
- T 继承了 Retrofit 提供的
Response
类,该类包含了 Http 返回值、协议版本号等等通用信息,R 是具体的业务数据结构,如同上文所说,R 继承了BaseResponse
,方便统一处理错误信息。 - 业务的错误返回值都是事先定义好的,只需要根据返回的错误码和错误类型进行处理即可
- 这里的错误类型本质上是利用 RxJava 的 map 转换方法,即将一种
Observable
转换成另一种Observable
,转换的过程中对不同的错误类型进行处理,同时将处理后的结果通过Observable
传递出去
2.2 线程切换
正如上文所言,之前的请求方法在请求回调方法中无法方便的切换线程,必须要借助 Android 原生的Handler
方式,代码就会比较分散,可读性也比较差,而 RxJava 针对线程切换提供了很友好的处理方法,只需要显式的设置即可完成不同线程的切换。
public class SchedulerTransformer<T> implements Observable.Transformer<T, T> {
@Override
public Observable<T> call(Observable<T> observable) {
return observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}
上面的代码主要完成了两部分的工作,subscribeOn
声明生产者Observable
所在的线程,由于网络请求比较耗时,一般会放到 IO 线程或者单独的子线程;而observeOn
声明了处理请求返回值所在的线程为 Android 系统的主线程,也即 UI 线程,这样就可以在处理返回值时更新 UI。
2.3 请求日志
网络请求经常会出现各种各样的异常情况,这个时候就需要通过查看请求日志来跟踪定位问题,OkHttp 提供了打印完整日志的方法,方便开发者调试网络请求。OkHttp 官方提供了一个很方便查看日志的Interceptor
,你可以控制你需要的打印信息类型,使用方法也很简单。
首先需要在 build.gradle 文件中引入 logging-interceptor
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
在 OkHttpClient 创建处添加创建好的Interceptor
即可,完整的示例代码如下:
private static OkHttpClient getNewClient(){
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
return new OkHttpClient.Builder()
.addInterceptor(logging)
.build();
}
HttpLoggingInterceptor
提供了 4 中控制打印信息类型的等级,分别是 NONE,BASIC,HEADERS,BODY,接下来分别来说一下相应的打印信息类型。
- NONE
没有任何日志信息
* Basic
打印请求类型,URL,请求体大小,返回值状态以及返回值的大小
D/HttpLoggingInterceptor$Logger: --> POST /upload HTTP/1.1 (277-byte body)
D/HttpLoggingInterceptor$Logger: <-- HTTP/1.1 200 OK (543ms, -1-byte body)
- Headers
打印返回请求和返回值的头部信息,请求类型,URL 以及返回值状态码
<-- 200 OK https://your_url (3787ms)
D/OkHttp: Date: Sat, 06 Aug 2016 14:26:03 GMT
D/OkHttp: Content-Type: application/json; charset=utf-8
D/OkHttp: Transfer-Encoding: chunked
D/OkHttp: Connection: keep-alive
D/OkHttp: Keep-Alive: timeout=30
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
D/OkHttp: Pragma: no-cache
D/OkHttp: Cache-Control: must-revalidate, no-cache, private
D/OkHttp: Set-Cookie: bid=D6UtQR5N9I4; Expires=Sun, 06-Aug-17 14:26:03 GMT; Domain=.douban.com; Path=/
D/OkHttp: X-DOUBAN-NEWBID: D6UtQR5N9I4
D/OkHttp: X-DAE-Node: dis17
D/OkHttp: X-DAE-App: book
D/OkHttp: Server: dae
D/OkHttp: <-- END HTTP
- Body
打印请求和返回值的头部和body
信息
<-- 200 OK https://your_url (3583ms)
D/OkHttp: Connection: keep-alive
D/OkHttp: Date: Sat, 06 Aug 2016 14:29:11 GMT
D/OkHttp: Keep-Alive: timeout=30
D/OkHttp: Content-Type: application/json; charset=utf-8
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
D/OkHttp: Transfer-Encoding: chunked
D/OkHttp: Pragma: no-cache
D/OkHttp: Connection: keep-alive
D/OkHttp: Cache-Control: must-revalidate, no-cache, private
D/OkHttp: Keep-Alive: timeout=30
D/OkHttp: Set-Cookie: bid=ESnahto1_Os; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: X-DOUBAN-NEWBID: ESnahto1_Os
D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
D/OkHttp: X-DAE-Node: dis5
D/OkHttp: Pragma: no-cache
D/OkHttp: X-DAE-App: book
D/OkHttp: Cache-Control: must-revalidate, no-cache, private
D/OkHttp: Server: dae
D/OkHttp: Set-Cookie: bid=5qefVyUZ3KU; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
D/OkHttp: X-DOUBAN-NEWBID: 5qefVyUZ3KU
D/OkHttp: X-DAE-Node: dis17
D/OkHttp: X-DAE-App: book
D/OkHttp: Server: dae
D/OkHttp: {"count":3,"start":0,"total":778,"books":[{"rating":{"max":10,"numRaters":202900,"average":"9.0","min":0},"subtitle":"","author":["[法] 圣埃克苏佩里"],"pubdate":"2003-8","tags":[{"count":49322,"name":"小王子","title":"小王子"},{"count":41381,"name":"童话","title":"童话"},{"count":19773,"name":"圣埃克苏佩里","title":"圣埃克苏佩里"}
D/OkHttp: <-- END HTTP (13758-byte body)
2.3 统一添加 token 和 User-Agent
现在基本上所有的应用都会通过token
来鉴定用户权限,User-Agent
参数方便服务端获取更多手机本地的信息,类似这样的每一个请求都需要的参数,也可以通过Interceptor
方式实现。
public class LoginInterceptor implements Interceptor {
public static final String ACCESS_TOKEN = "access_token";
public static final String USER_AGENT = "User-Agent";
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header(USER_AGENT, AppUtils.getUserAgent() + " app_name/" + AppUtils.getVersionName())
.header(ACCESS_TOKEN, AppUtils.getToken())
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
}
2.4 通过对象封装请求体
有的时候post
请求的参数会有很多个,如果一个个写,那么方法的参数就显得很多,杂乱且不好维护,当然可以用Map
的方式集中管理,但是Map
有一个问题就是一旦key
值写错了,前期是无法及时发现的,必须等到真正的网络请求的时候才能发现。Retrofit
提供了一个很好的解决方法,通过对象封装这些请求参数。
@FormUrlEncoded
@POST("book/reviews")
Observable<Response<String>> addReviews(@Body Reviews reviews);
public class Reviews {
public String book;
public String title;
public String content;
public String rating;
}
需要注意的是类中的属性名必须要和请求参数的 key 保持一致,否则服务端无法正常识别请求参数
关于 Retrofit 详细的用法可以参考文章Retrofit 详解
3. 如何使用
我们以获取Category
为例来说明如何利用Retrofit
和RxAndroid
来改写现有模块。
3.1 定义 CategoryResponse
CategoryResponse
必须继承自BaseResponse
,里面包含了错误信息的数据结构。
@Keep
public class CategoryResponse extends BaseResponse {
public Response response;
@Keep
public static final class Response {
public List<Category> categories;
}
}
其中Category
是具体的实体类型。
3.2 定义 Service Method
public interface TradesService {
@GET("api/tradecategories/get")
Observable<Response<CategoryResponse>> tradeCategories();
}
注意点
TradesService
必须是一个interface
,而且不能继承其他interface
。tradeCategories
的返回值必须是Observable
类型。
3.3 利用 ServiceFactory 创建一个 TradeService 实例
在适当的时机(Activity#onCreate
、Fragment#onViewCreated
等)根据网关类型通过ServiceFactory
创建一个TradeService
实例。
mTradesService = ServiceFactory.createNewService(TradesService.class)
3.4 TradeService 获取数据
mTradesService.tradeCategories()
.compose(new DefaultTransformer<CategoryResponse>(getActivity()))
.map(new Func1<CategoryResponse, List<Category>>() {
@Override
public List<Category> call(CategoryResponse response) {
return response.response.categories;
}
})
.flatMap(new Func1<List<Category>, Observable<Category>>() {
@Override
public Observable<Category> call(List<Category> categories) {
return Observable.from(categories);
}
})
.subscribe(new ToastSubscriber<Category>(getActivity) {
@Override
public void onCompleted() {
hideProgressBar();
// business related code
}
@Override
public void onError(Throwable e) {
super.onError(e);
hideProgressBar();
// business related code
}
@Override
public void onNext(Category category) {
// business related code
}
});
注意:DefaultTransformer
包含了线程分配和错误处理两部分功能,所以调用方只需要关心正确的数据就可以了。
DefaultTransformer
包含了上文提到的线程切换转换器SchedulerTransformer
和错误处理转换器ErrorCheckerTransformer
,具体代码如下:
public class DefaultTransformer<R extends BaseResponse>
implements Observable.Transformer<Response<R>, R> {
private Context context;
public DefaultTransformer(final Context context) {
this.context = context;
}
@Override
public Observable<R> call(Observable<Response<R>> observable) {
return observable
.compose(new SchedulerTransformer<Response<R>>())
.compose(new ErrorCheckerTransformer<Response<R>, R>(context));
}
}
4. 写在最后
网络请求对于提升 Android 应用的体验和性能有很大的影响,结合 Retrofit 使用提高了代码可读性和编码的灵活性,RxJava 提供了链式调用方式,融合了线程切换、数据过滤、数据转换等优点。有赞在此基础上进行了少量的封装,便可适应大部分复杂的业务场景。