聊聊对RxJava与Retrofit的封装

作者: _SOLID | 来源:发表于2016-08-16 13:26 被阅读10569次

    目前RxJava和Retrofit结合使用已经是非常普遍了,网上关于这方面的文章也是层出不穷,其实大致的思想都是差不多的,今天我也来写一篇关于RxJava与Retrofit的文章,聊一聊关于RxJava与Retrofit的封装,尽可能的能让其适用于大部分项目,以供大家在学习这方面的时候多一份参考。

    关于RxJava的基础使用可以参考我的另一篇文章:是时候学习RxJava了,至于Retrofit的基本使用这里我就不做介绍了,这里可以给大家提供一个学习Retrofit比较全面的网址retrofit-getting-started-and-android-client,对Retrofit还不太熟悉的同学可以先去看看上面的系列文章。

    闲话不多说了,直接上今天的主题。我们先来看看关于RxJava和Retrofit最基本的使用是怎么样的

    首先我们需要去定义一个对应接口的Service和返回结果的实体类

    public class GankResultBean {
        private boolean error;
        private List<ResultsBean> results;
        ...省略部分代码...
    }
     public interface RxGankService {
            @GET("data/all/20/{page}")
            Observable<GankResultBean> getAndroidData(@Path("page") int page);
     }
    

    接着再去初始化Retrofit

    Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl("http://gank.io/api/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .build();
    

    最后我们就可以去使用了

     RxGankService rxGankService = retrofit.create(RxGankService.class);
     Observable<GankResultBean> observable = rxGankService.getAndroidData(1);
     observable.subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Subscriber<GankResultBean>() {
                        @Override
                        public void onCompleted() {
    
                        }
    
                        @Override
                        public void onError(Throwable e) {
    
                        }
    
                        @Override
                        public void onNext(GankResultBean gankResultBean) {
    
                        }
                    });
    

    逻辑还是挺清晰的,但是呢,如果每次使用都让你去写这么多的代码肯定会觉得很乏味,并且这里我还没有对返回结果做错误处理,于是我们就应该考虑一下对代码封装一下了。

    但是应该从哪里入手呢,这里简单分析下:

    • 看上面的代码我们会发现Retrofit初始化的那段代码,一般就一个baseUrl会有不同,其他的基本是一致的,如果我们每次去创建一个Service都要去写那么多重复的代码也大大增加了冗余度
    • 对于返回的结果一般情况下数据格式是这样的:
      {
         code:1,
         msg:"your message",
         data:[]
      }
    

    code是服务器端和客户端约定好的一种规则,比如1表示数据请求成功,-1表示请求失败,-2表示权限不足等等,msg代表提示消息,其中data可能是数组对象也可能是普通的对象,我们可以考虑对返回的结果做一个统一的处理。

    经过上面的分析我们大致有了一个方向,对于Service的创建应该有一个类去单独处理。所以这里我创建了一个ServiceFactory的类。

    /**
     * Created by _SOLID
     * Date:2016/7/27
     * Time:15:23
     */
    public class ServiceFactory {
    
        private final Gson mGsonDateFormat;
    
        private ServiceFactory() {
            mGsonDateFormat = new GsonBuilder()
                    .setDateFormat("yyyy-MM-dd hh:mm:ss")
                    .create();
        }
    
        private static class SingletonHolder {
            private static final ServiceFactory INSTANCE = new ServiceFactory();
        }
    
        public static ServiceFactory getInstance() {
            return SingletonHolder.INSTANCE;
        }
    
        /**
         * create a service
         *
         * @param serviceClass
         * @param <S>
         * @return
         */
        public <S> S createService(Class<S> serviceClass) {
            String baseUrl = "";
            try {
                Field field1 = serviceClass.getField("BASE_URL");
                baseUrl = (String) field1.get(serviceClass);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.getMessage();
                e.printStackTrace();
            }
            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(baseUrl)
                    .client(getOkHttpClient())
                    .addConverterFactory(GsonConverterFactory.create(mGsonDateFormat))
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .build();
            return retrofit.create(serviceClass);
        }
    
        private final static long DEFAULT_TIMEOUT = 10;
    
        private OkHttpClient getOkHttpClient() {
            //定制OkHttp
            OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
            //设置超时时间
            httpClientBuilder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
            httpClientBuilder.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
            httpClientBuilder.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
            //设置缓存
            File httpCacheDirectory = new File(FileUtils.getCacheDir(SolidApplication.getInstance()), "OkHttpCache");
            httpClientBuilder.cache(new Cache(httpCacheDirectory, 10 * 1024 * 1024));
            return httpClientBuilder.build();
        }
    }
    

    其他的代码我就不做说明了,这里我只对createService方法做一个简单的说明:对于baseUrl是用反射去获取我们自定义Service中的BASE_URL字段,所以在使用的时候就有了一个约定,当我们新建一个Service的时候一定要有BASE_URL字段并赋值。也就是说我们在新建的Service大致是这样的

    public interface GankService {
    
        String BASE_URL = "http://www.gank.io/api/";
    
       @GET("data/all/20/{page}") Observable<GankResultBean> getAndroidData(@Path("page") int page);
    }
    

    现在我们应该怎样去使用呢

    GankService gankService = ServiceFactory.getInstance().createService(GankService.class);
    

    是不是一下感觉创建一个Service的代码一下简洁了很多。这里还没完,我们只是解决了Service的创建,还没有对结果去做处理。

    我以 http://gank.io/api/data/Android/10/1 这个接口为例:
    他返回的结果的格式是这样的:

    {
      "error": false, 
      "results": []
    }
    

    所以这里我定义了这样的一个泛型类(T 是返回结果results的类型)

    public class HttpResult<T> {
        public boolean error;
        public T results;
    }
    

    在处理结果的时候,其实用户只关心的是T,对其他数据可以统一处理下就比如这里的error字段,有了这个我们就可以再封装一下Subscriber了。

    public abstract class HttpResultSubscriber<T> extends Subscriber<HttpResult<T>> {
    
        @Override
        public void onCompleted() {
    
        }
    
        @Override
        public void onError(Throwable e) {
            Logger.e(this,e.getMessage());
            e.printStackTrace();
            //在这里做全局的错误处理
            if (e instanceof HttpException) {
                // ToastUtils.getInstance().showToast(e.getMessage());
            }
            _onError(e);
        }
    
        @Override
        public void onNext(HttpResult<T> t) {
            if (!t.error)
                onSuccess(t.results);
            else
                _onError(new Throwable("error=" + t.error));
        }
    
        public abstract void onSuccess(T t);
    
        public abstract void _onError(Throwable e);
    }
    

    我们来看看现在怎么使用吧:

    ServiceFactory.getInstance()
                    .createService(GankService.class)
                    .getAndroidData(1)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribeOn(Schedulers.io())
                    .subscribe(new HttpResultSubscriber<List<GanHuoDataBean>>() {
                        @Override
                        public void onSuccess(List<GanHuoDataBean> list) {
    
                        }
    
                        @Override
                        public void _onError(Throwable e) {
    
                        }
                    });
    

    把这段代码与文章开始那段代码比较一下,是不是神清气爽了很多,并且这里还做了错误处理的。

    细心的同学肯定注意到了这段代码,这段代码每次都是在重复的使用

    observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(Schedulers.io())
    

    这里我们可以创建一个TransformUtils类去处理一下

    public class TransformUtils {
    
        public static <T> Observable.Transformer<T, T> defaultSchedulers() {
            return new Observable.Transformer<T, T>() {
                @Override
                public Observable<T> call(Observable<T> tObservable) {
                    return tObservable.observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io());
                }
            };
        }
    
        public static <T> Observable.Transformer<T, T> all_io() {
            return new Observable.Transformer<T, T>() {
                @Override
                public Observable<T> call(Observable<T> tObservable) {
                    return tObservable.observeOn(Schedulers.io()).subscribeOn(Schedulers.io());
                }
            };
        }
    }
    

    然后在使用的时候使用compose操作符就可以了

    .compose(TransformUtils.<HttpResult<List<GanHuoDataBean>>>defaultSchedulers())
    

    我们都知道对于网络请求肯定会有不成功的情况,有没有一种方案能够处理一下?其实RxJava已经为我们提供了这样的一个操作符<code>RetryWhen</code>可以用来实现重试机制,我这里有一个实现好的一个机制,默认情况下,最多重试3次,第一次会等3s,第二次会等6s,第三次会等9s。

    public class RetryWhenNetworkException implements Func1<Observable<? extends Throwable>, Observable<?>> {
        private int count = 3;//retry count
        private long delay = 3000;//delay time
    
        public RetryWhenNetworkException() {
    
        }
    
        public RetryWhenNetworkException(int count) {
            this.count = count;
        }
    
        public RetryWhenNetworkException(int count, long delay) {
            this.count = count;
            this.delay = delay;
        }
    
        @Override
        public Observable<?> call(Observable<? extends Throwable> observable) {
            return observable
                    .zipWith(Observable.range(1, count + 1), new Func2<Throwable, Integer, Wrapper>() {
                        @Override
                        public Wrapper call(Throwable throwable, Integer integer) {
                            return new Wrapper(throwable, integer);
                        }
                    }).flatMap(new Func1<Wrapper, Observable<?>>() {
                        @Override
                        public Observable<?> call(Wrapper wrapper) {
                            if ((wrapper.throwable instanceof ConnectException
                                    || wrapper.throwable instanceof SocketTimeoutException
                                    || wrapper.throwable instanceof TimeoutException)
                                    && wrapper.index < count + 1) {
                                return Observable.timer(delay + (wrapper.index - 1) * delay, TimeUnit.MILLISECONDS);
                            }
                            return Observable.error(wrapper.throwable);
                        }
                    });
        }
    
        private class Wrapper {
            private int index;
            private Throwable throwable;
    
            public Wrapper(Throwable throwable, int index) {
                this.index = index;
                this.throwable = throwable;
            }
        }
    }
    

    到这里对于RxJava与Retrofit的封装的基本封装就差不多了,也能适用于大部分的项目中去了,一般情况下改改HttpResult和HttpResultSubscriber这两个类就可以了。
    但是实际开发中有可能会遇到这样的一种情况:直接去访问一个完整的Url,还有用Retrofit去做下载该怎么做呢?

    其实解决方案是有的,这里我们可以去定义一个CommonService

    public interface CommonService {
        String BASE_URL = "http://www.example.com/";//这个不重要,可以随便写,但是必须有
    
        @GET
        Observable<ResponseBody> loadString(@Url String url);
    
        @GET
        @Streaming
        Observable<ResponseBody> download(@Url String url);
    }
    

    其实就是把参数的注解换成@Url就可以了,关于实现的细节可以参考文末给出的源码。

    本文源码地址:源码

    参考资料可以去我管理的专题查看:
    RxJava系列专题(Android方向)

    相关文章

      网友评论

      • 海芋洋芋:为啥你的抽象类继承的接口?什么鬼?
      • TonyEasy:楼主,跟你请教一个问题,每次的请求数据都会产生一次的订阅关系,加入在主MainActivity中有很多接口进行请求,则会产生多次的订阅关系,而没有及时进行取消订阅,我猜测会造成内存溢出问题,请问这个问题怎么看?
      • 不再停留_44ce:RetryWhenNetworkException又该怎么使用呢
        _SOLID: @不再停留_44ce 你可以看一下我给的代码
      • A_si: String BASE_URL = "http://www.example.com/&quot;;这个确定了,如果我别的域名下的图片要下载。怎么写
      • A_si:我原来就是照着你的写的。后来换了版本就不行了
        _SOLID:@A_si 你可以参考下我最新的代码
      • A_si: compile 'com.squareup.retrofit2:adapter-rxjava
        compile 'com.squareup.retrofit2:converter-gson
        或者retrofit的版本换了。就不行了。楼主有没有试过rxjava2
        _SOLID:@A_si 就是源码的那个项目,你切换到主分支,最新的就是rxjava2
      • f3f4672ef98e:源码打不开了 楼主大大
        _SOLID:@全幽生 好的,有时间我加一下更新信息。如果更倾向于老版本的话,可以去下载源码,自己编译安装的:smile:
        f3f4672ef98e:@_SOLID 话说这次干货客户端升级版本后 个人感觉没之前好看了 哈哈 个人之见:relaxed: 不过希望可以在《[干货IO 3.0] 一个完全开源的App》加上这次升级版本有哪些具体变化
        _SOLID:多谢提醒,现在可以访问了。由于最近我把源码更新到了rxjava2,更改了一下目录结构,所以导致不能访问
      • DreamArea:ServiceFactory单例写错了吧?构造方法应该私有的吧,public修饰外部可直接new啊?是我理解错了么?
        DreamArea:@_SOLID :stuck_out_tongue_closed_eyes: :+1:
        _SOLID:@DreamArea 已经修改
        _SOLID:你看的真仔细,确实应该是private
      • Evil_c2e1:您好,我把HttpResult改成我自己接口的内容这样
        public String code;
        public String message;
        public T content;
        其他什么都没改然后我请求接口成功,但是断网之后读不到缓存了
        HTTP 504 Unsatisfiable Request (only-if-cached)
        _SOLID:@Evil_c2e1 你可以去看看我最新的代码,之前的这个例子只是设置了缓存的位置,并没有做缓存相关的处理(需要添加拦截器)。
      • 844b9a3a3a68:是我调用姿势不对么?
        ObservableProvider.getDefault().<List<ResultsBean>>loadResult("http://gank.io/api/data/Android/10/1)
        .subscribe(new HttpResultSubscriber<List<ResultsBean>>() {
        @Override
        public void onSuccess(List<ResultsBean> resultsBeen) {
        for (int i = 0; i < resultsBeen.size(); i++) {
        System.out.println(resultsBeen.get(i).getDesc());
        }
        }

        @Override
        public void _onError(Throwable e) {
        //System.out.println(e);
        }
        });
        844b9a3a3a68: @有梦想的程序丶猿
        泛型擦除原因,导致Gson解析不到指定的实体类,所以回调回来的数据并不是泛型类型。
      • c66243381c8b:请问 "success": false,
        "data": {
        "name": "Unauthorized",
        "message": "access-token是一个无效的凭证.",
        "code": 0,
        "status": 401
        }
        请问 如果我要得到服务器返回的错误message 但这个message 是来自服务器返给我的数据
        我该怎么得到他 (类似于 点赞 这类服务器返回的message )
        _SOLID:@melonz 你将data里面的数据定义成一个实体类就行了
        c66243381c8b:@_SOLID 如果msg 在外面 可以直接获取 可现在 这个msg 在res 里面 我就不会怎么在此解析了拿到这个msg了 请问 我该怎么做 刚接触rxjava
        _SOLID:@melonz 这个直接获取不就行了吗?
      • 酒吞:这几天刚看完RxJava文档 看了之后 有一点不明白的是
        observeOn 跟 subscribeOn 难道不是 observeOn之后再调用subscribeOn 并不会有实质性的效果的么?
      • db59667c4a37: 那我要在doOnNext中做数据库操作,还要制定线程的,这样封装不满足呀,该怎么办呢,楼主?
        _SOLID: @db59667c4a37 这里只是给一个封转思路,而不是完全按照这个来,你说的这种情况,可以自己去切换线程。
      • 吃鱼的黄蜂:请问,添加了请求取消这个功能么
        db59667c4a37:那我要在doOnNext中做数据库操作,还要制定线程的,这样封装不满足呀,该怎么办呢,楼主?
        _SOLID:@吃鱼的黄蜂 Rx取消订阅就可以了
      • Mr_banzhuan:请问如果 results 返回的不是Jsonarray,是一个JsonObjeoct,改怎么去封装。
        public class ResultsEntity {
        private String token;
        private String name;
        private String schoolName;
        private String imagePath;
        private DefaultRoleEntity defaultRole;
        private List<RolesEntity> roles;
        results是这样的,改怎么去封装
        _SOLID:@宇峰2333 这个不就是类似于我文章中的那个例子吗?把HttpResultSubscriber稍稍修改下就ok了
        Mr_banzhuan:@_SOLID 那用什么比较好,我的返回时public class HttpResult<T> {
        public int code;
        public String msg;
        public T data;
        },主要需要对code的值进行处理,然后判断服务器返回的状态,是调用成功,还是提交数据无效,我主要就是对这个code处理进行封装
        _SOLID:@宇峰2333 返回的是对象就不用HttpResultSubscriber了
      • Mr_banzhuan:mark一下,这个封装的正好,之前看见的一个封装,封装过度了
      • 29778cb33978:请问你们在公司已经开始用了吗?还是用的其他的网络请求框架。
        _SOLID: @乐乐00000 我现在的项目已经在用了
      • 29778cb33978:那嵌套请求呢?比如我第一个service接口请求回的数据作为第二个Service接口请求的参数。那该怎么封装呢?
        _SOLID: @乐乐00000 这个可以自己做项目的时候,根据具体情况去处理,平时的封装也不能过度
      • 0a4eb6ff8445:请问一下,如何获取到尚未解析的JSON数据,比方说我一开始并不知道后台返回回来的数据是怎样的?
        _SOLID:@0a4eb6ff8445 不使用本文的封装返回的就是请求的Json字符串
      • OneBelowZero:封装的很好 mark一下 看看源码

      本文标题:聊聊对RxJava与Retrofit的封装

      本文链接:https://www.haomeiwen.com/subject/wviusttx.html