Retrofit2RxjavaDemo

Introduction: Retrofit2 结合 Rxjava 解决返回的 JSON 结果为非 Rest 标准后,返回码为正确下使用 Gson 返回真实的数据,保留了 Rxjava 的链式调用,而不是采用回调的方式,最大可能发挥 Rxjava 的特点。
More: Author   ReportBugs   
Tags:
Rxjava-Retrofit2-网络请求-Gson-Restful-Demo-

Build Status


Help

谁愿意使用本 Demo 后有时间,希望有人可以在使用本 Demo 后发有时间来帮助,愿意做 contributor 的联系我 ysmintor@gmail.com。

无 Dagger2 示例

Dagger2 的使用可以简化相关依赖,所以把 Dagger2 使用集成在一起,所以之前的无 dagger2 放在nodagger2 branch中,不使用 Dagger2 的可以查看这个分支。

效果展示

rx common

本文主要介绍了使用 Retrofit2 配合 Rxjava[这里指 Rxjava1, 暂时未适配 Rxjava2] 来处理非 restful 的网络请求结果。一般而言请求的非 REST 结果如下,包括一个 code 代表结果的状态和 msg 表示描述,以及 data 对应的真实的数据。我们的目的就是直接取data对应的数据,如果是数组那么取的也是数组list<databean>类型的,如果有错误,那么就会调用 onError()方法。更多演示效果请查看video目录下的视频。

{
    "code": 0,    //请求返回状态码 0 表示成功,其它则为对应的错误类型
    "msg": "请求成功",    //请求返回状态
    "data":
    {    //返回结果
        "phone": "010-62770334;010-62782051",    //电话
        "website": "www.tsinghua.edu.cn",    //官网
        "email": "zsb@mail.tsinghua.edu.cn",    //邮箱
        "address": "北京市海淀区清华大学",    //地址
        "zipcode": "0102770334",    //邮编
        "name": "清华大学",    //学校名称
        "img": "http://img.jidichong.com/school/3.png",    //学校 logo 图片
        "parent": "教育部",    //隶属部门
        "type": " 211 985",    //学校类型
        "profile": "xasd",   //简介
        "info": "院士:68 人 博士点:198 个 硕士点:181 个",    //说明
        "city": "北京"   //所在省市
    }
}

而 Retrofit2 请求的结果一般都分为 Header 和 Body。在获取这些数据后再在 Rxjava subscriber 中的 onNext()来处理比较麻烦。所以这个 demo 介绍了如何结果 Retrofit2 与 Rxjava1 来处理数据并在成功时得到 data 字段的数据,服务端返回有错误是进入 onError(),当是单个实体的时候,在 onNext 中就直接得到这个结果,如果是一个数组的时候,则是返回 List 这种形式。同时也简化了定义 gson 实体不用每次都加 code, msg 这样一层。


使用方法:

1. 配置对应的外层实体。

例如下面。开发中一般都非标准的 REST 都是一个数据(百度开放平台的接口就基本都是这种形式),一个状态码和一个消息。其中 data 的类型是泛型,可以在生成请求的 api 指定实际返回的类型,如果为空的情况可以使用 String。 而 code 和 message 是对应于服务端定义的代码 code 和返回的错误信息。如果你的后台后台字段不同,你可以按需要修改这个 HttpResult 的相应字段。

    {
        // code 为返回的状态码, message 为返回的消息, 演示的没有这两个字段,考虑到真实的环境中基本包含就在这里写定值
        private int code = 0;
            private String message = "OK";

        //用来模仿 Data
        @SerializedName(value = "subjects")
        private T data;
    }

2. 同 Retrofit2 一样要定义接口。

如下。这里仅仅是有 GET 的接口在 demo 里,POST, PUT, DELETE, QUERY 等都是一样的。HttpResult里 T 的类型就是指定泛型 data 的具体类型。可以使用 String, JSONObject,定义的实体等等。

另外有朋友问题访问参数是 JSON 对象怎么办 (Body Paramter JSON Object)?这其实就是将你的参数设置成一个已经定义的实体,我也给出一个项目中的接口。下面的 post 就是这种方式。关于请求的 REST 方式我会在文章后面放出详细的参考,若你不熟悉请参考这些文章。


@GET("mock3")
Observable<HttpResult<MockBean>> getMock3();

@GET("mock1")
Observable<HttpResult<List<MockBean>>> getMock1();

@GET("mock4")
Observable<HttpResult<MockBean>> getMock4();

@GET("mock2")
Observable<HttpResult<MockBean>> getMock2();

3. 请求网络。

直接调用

public static MockApi mockApi() {
    return ServiceFactory.createService(MockApi.class);
}

使用一个默认的.compose(new DefaultTransformer<List<MockBean>>())可以非常方便地进行转化成了需要的Observable。如下代码中那样进行了线程的转换,错误的处理在这个 transformer,可以自定义自己的 transformer。

return observable
          .subscribeOn(Schedulers.io())
          .observeOn(Schedulers.newThread())
          .compose(ErrorTransformer.<T>getInstance())
          .observeOn(AndroidSchedulers.mainThread());

另外准备了常用的 subscriber,包含了网络连接的错误处理,例如非 200 状态,另外是服务端(业务)错误的处理,默认是将错误编码和错误信息在控制台和手机上输出。 提供的RxSubscriberCommonScriber中的onNext()必须实现。

建议使用RxLifecycle防止使用RxJava内存泄露。其它方面使用同 RxJava 与 Retrofit2 结合的使用是相同的,所以得到结果您若有若要对数据进行处理仍然是链式调用。


关于错误处理方面介绍

主要使用了 RxJava 中的onErrorResumeNext,遇到错误后将错误通过ExceptionEngine.handleException(throwable)进行处理。

@Override
public Observable<T> call(Observable<HttpResult<T>> responseObservable) {
    return responseObservable.map(new Func1<HttpResult<T>, T>() {
        @Override
        public T call(HttpResult<T> httpResult) {
            // 通过对返回码进行业务判断决定是返回错误还是正常取数据
            if (httpResult.getCode() != ErrorType.SUCCESS) throw new ServerException(httpResult.getMessage(), httpResult.getCode());
            return httpResult.getData();
        }
    }).onErrorResumeNext(new Func1<Throwable, Observable<? extends T>>() {
        @Override
        public Observable<? extends T> call(Throwable throwable) {
            //ExceptionEngine 为处理异常的驱动器
            throwable.printStackTrace();
            return Observable.error(ExceptionEngine.handleException(throwable));
        }
    });
}

其中的 ApiException 包括 code 和错误的详情

public class ApiException extends Exception {
    // 异常处理,为速度,不必要设置 getter 和 setter
    public int code;
    public String message;

    public ApiException(Throwable throwable, int code) {
        super(throwable);
        this.code = code;
    }
}

重点在于下面这个处理。你可以再定义自己的业务相关的错误,目前常用的都已经有了,而业相关的错误大部分通过接收的数据直接显示了。若不是直接显示的情况,你完全可以在提供的 subscriber 里去实现。

public class ExceptionEngine {
    //对应 HTTP 的状态码
    private static final int UNAUTHORIZED = 401;
    private static final int FORBIDDEN = 403;
    private static final int NOT_FOUND = 404;
    private static final int REQUEST_TIMEOUT = 408;
    private static final int INTERNAL_SERVER_ERROR = 500;
    private static final int BAD_GATEWAY = 502;
    private static final int SERVICE_UNAVAILABLE = 503;
    private static final int GATEWAY_TIMEOUT = 504;

    public static ApiException handleException(Throwable e){
        ApiException ex;
        if (e instanceof HttpException){             //HTTP 错误
            HttpException httpException = (HttpException) e;
            ex = new ApiException(e, ErrorType.HTTP_ERROR);
            switch(httpException.code()){
                case UNAUTHORIZED:
                    ex.message = "当前请求需要用户验证";
                    break;
                case FORBIDDEN:
                    ex.message = "服务器已经理解请求,但是拒绝执行它";
                    break;
                case NOT_FOUND:
                    ex.message = "服务器异常,请稍后再试";
                    break;
                case REQUEST_TIMEOUT:
                    ex.message = "请求超时";
                    break;
                case GATEWAY_TIMEOUT:
                    ex.message = "作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI 标识出的服务器,例如 HTTP、FTP、LDAP)或者辅助服务器(例如 DNS)收到响应";
                    break;
                case INTERNAL_SERVER_ERROR:
                    ex.message = "服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理";
                    break;
                case BAD_GATEWAY:
                    ex.message = "作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应";
                    break;
                case SERVICE_UNAVAILABLE:
                    ex.message = "由于临时的服务器维护或者过载,服务器当前无法处理请求";
                    break;
                default:
                    ex.message = "网络错误";  //其它均视为网络错误
                    break;
            }
            return ex;
        } else if (e instanceof ServerException){    //服务器返回的错误
            ServerException resultException = (ServerException) e;
            ex = new ApiException(resultException, resultException.code);
            ex.message = resultException.message;
            return ex;
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException){
            ex = new ApiException(e, ErrorType.PARSE_ERROR);
            ex.message = "解析错误";            //均视为解析错误
            return ex;
        }else if(e instanceof ConnectException || e instanceof SocketTimeoutException || e instanceof ConnectTimeoutException){
            ex = new ApiException(e, ErrorType.NETWORD_ERROR);
            ex.message = "连接失败";  //均视为网络错误
            return ex;
        }
        else {
            ex = new ApiException(e, ErrorType.UNKNOWN);
            ex.message = "未知错误";          //未知错误
            return ex;
        }
    }
}

关于处理服务器在错误时将错误信息直接放在 data 字段,即 data 字段在结果成功和失败对应的类型不定。处理思路是自定义 GsonConverter,可以查看 Demo 里的MockDataActivity去看使用方法,其实就修改 Retofit2 初始化传入的 GsonConverter。关键是对于CustomGsonResponseBodyConverter的修改。

@Override
public T convert(ResponseBody value) throws IOException {
    String response = value.string();
    JsonElement jsonElement = jsonParser.parse(response);
    int parseCode = jsonElement.getAsJsonObject().get("code").getAsInt();
    //
    if (parseCode != ErrorType.SUCCESS) {
        value.close();
        String msg = jsonElement.getAsJsonObject().get("data").getAsString();
        throw new ServerException(msg, parseCode);
    } else {

        MediaType contentType = value.contentType();
        Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
        InputStream inputStream = new ByteArrayInputStream(response.getBytes());
        Reader reader = new InputStreamReader(inputStream, charset);
        JsonReader jsonReader = gson.newJsonReader(reader);

        try {
            return adapter.read(jsonReader);
        } finally {
            value.close();
        }
    }
}

这里先解析 code 字段再进行判断,所以处理这种服务器返回的话,是需要将上面的"code""data"替换成你服务端具体的字段。


Update

2017-07-12

增加了Dagger 2.11(依赖注入)的使用,非Dagger2方式放在不同的Branch noDagger2中方便不使用 Dagger2 用户

2017-03-28

更新说明文档,修正格式化错位,解决在 atom 下 tab 长度与 github 长度不对应的显示问题。

2017-03-27

更新文档,增加短视频说明,增加错误信息放在 data 字段的说明。

2017-03-07

添加处理非 REST 接口在 token 失效时或 code 异常时,错误信息放在 data 字段的解析办法处理。

 {
    code:-1
    data:"token 失效"
 }
{
    code:0
    data:{name:"xiaoming", age:23}
}

针对上面的 JSON 都要在同一个接口里处理,解决办法都是两次解析的办法,第一次取到 code 并且判断,不是期望的值进行处理。期望的值可按原路处理。这里采用了修改 GsonConverter 的办法。

2016-12-26

解决了执行 onCompleted()之后执行 onError()的问题
if (!isUnsubscribed())
{
    unsubscribe();
}

2016-10-13

修正了服务端 code 没有处理,返回为错误时认为是 json 实体解析问题。

if (httpResult.getCode() != ErrorType.SUCCESS || httpResult.getCode() != ErrorType.SUCCESS)

Thanks

Contact Me

  • Github: github.com/ysmintor
  • Email: ysmintor@gmail.com

License

Copyright 2016 - 2017 YorkYu. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Support Me
Apps
About Me
Google+: Trinea trinea
GitHub: Trinea