文档章节

Rxjava2 + Retrofit +DBflow + 自定义缓存框架搭建

yizhihaohut
 yizhihaohut
发布于 2017/08/26 01:05
字数 2975
阅读 392
收藏 1

最喜欢干的事,莫过于拿着工资搭框架了。

其实这个框架已经出来很久了,并不是什么新鲜玩意儿了,只不过我一直没有尝试着去写一篇内容比较大的文章来分享,这次就卖弄一下,希望各种大神轻喷,有什么问题也希望各位大神不吝赐教。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~正题分割线~~~~~~~~~~~~~~~~~~~~~~~~~~

  • Retrofit的接入

 ApiService

首先Retrofit的框架架构搭建其实比较简单,因为Retrofit本身已经极致简单了。

/**
 * Author       : yizhihao (Merlin)
 * Create time  : 2017-08-23 15:48
 * contact      :
 * 562536056@qq.com || yizhihao.hut@gmail.com
 */
public interface ApiService {

    @GET("{url}")
    Observable<ResponseBody> executeGet(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);


    @POST("{url}")
    Observable<ResponseBody> executePost(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);

    @POST("{url}")
    Observable<ResponseBody> executeCachePost(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);

    @POST("{url}")
    Observable<ResponseBody> uploadFiles(
            @Path("url") String url,
            @Path("headers") Map<String, String> headers,
            @Part("filename") String description,
            @PartMap()  Map<String, RequestBody> maps);

    @Streaming
    @GET
    Observable<ResponseBody> downloadFile(@Url String fileUrl);
}

retrofit接口对象

上面的代码是将接口返回类型通用化并且结合返回rxjava的Observable这样我们就可以愉快的用rxjava来处理线程切换了。

public static Retrofit retrofit() {
        return retrofit(sBaseUrl);
    }

    public static Retrofit retrofit(String baseUrl) {
        return new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(getInstance().getHttpClient())//添加自定义OkHttpClient
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(GsonUtils.getInstance().getGson()))
                .build();
    }

    public OkHttpClient getHttpClient() {
        if (client == null) {
            client = new OkHttpClient.Builder()
               
                    //.addNetworkInterceptor(new CacheNetworkInterceptor())
                    .addInterceptor(new LoggingInterceptor()) //日志,可以配置 level 为 BASIC / HEADERS / BODY
                    .connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS)
                    //.cache(provideCache())
                    .retryOnConnectionFailure(true)
                    .build();
        }
        return client;
    }

缓存拦截器

CacheNetworkInterceptor这个本来要添加的NetworkInterceptor是为了做缓存Hook的

但是查阅了一些资料,还有okhttp源码,其实okhttp本身是自带缓存逻辑的,这套逻辑完全遵守RFC协议进行缓存控制的。很多人都去hook掉了这步。其实查阅源码可以看到

我们在调用cache的时候okhttp会把自己的internalCache给废弃掉,而我们在okhttp的内部拦截器中也会看到CacheInterceptor,这个类其实就是实现了okhttp的Cache-control。所以我并没有选择去拦截Response手动添加Cache-control进行缓存处理。当然大家要用我也拦不住,毕竟也挺方便的。

日志拦截器

而LoggingInterceptor拦截器主要是为了打印请求发送和收到请求的Log.

public class LoggingInterceptor implements Interceptor {

    private boolean debugMode = DebugConstant.isDebug;

    @Override
    public Response intercept(Chain chain) throws IOException {
        if(!debugMode){
            return chain.proceed(chain.request());
        }
        //这个chain里面包含了request和response,所以你要什么都可以从这里拿
        Request request = chain.request();

        long t1 = System.nanoTime();//请求发起的时间
        LogUtils.e(String.format("发送请求 %s on %s%n%s", request.url(), chain.connection(), request.headers()));

        Response response = chain.proceed(request);

        long t2 = System.nanoTime();//收到响应的时间

        //这里不能直接使用response.body().string()的方式输出日志
        //因为response.body().string()之后,response中的流会被关闭,程序会报错,我们需要创建出一
        //个新的response给应用层处理
        ResponseBody responseBody = response.peekBody(1024 * 1024);

        LogUtils.d(String.format("接收响应: [%s]" +
                        "\n %n返回json:【%100s】 " +
                        "\n请求执行时间%.1fms" +
                        "\n%n%s",
                response.request().url(),
                responseBody.string(),
                (t2 - t1) / 1e6d,
                response.headers()));
        return response;
    }

}

看到打印出来的详细的log有木有感觉很酸爽。

  • Rxjava的封装

绑定Activity生命周期

retrofit基本讲解完毕。然后是对rxjava中的subcriber的封装,这里主要是将activity的生命周期和subcriber绑定联系起来,当activity被finish的时候我们的subcriber也应该dispose取消掉。

    private CompositeDisposable disposables2Stop;// 管理Stop取消订阅者者
    private CompositeDisposable disposables2Destroy;// 管理Destroy取消订阅者者

在baseaActivity中通过CompositeDisposable组合管理添加进来的Disposable。然后在ondestroy中进行统一取消,防止内存泄漏。

 @Override
    protected void onDestroy() {
        super.onDestroy();
        if (disposables2Destroy == null) {
            throw new IllegalStateException(
                    "onDestroy called multiple times or onCreate not called");
        }
        disposables2Destroy.dispose();
        disposables2Destroy = null;
        if (mDelegate != null) {
            mDelegate.ondestroy();
            mDelegate = null;
        }
    }

基类订阅者BaseObserver

通用BaseObserver是继承于rxjava的Observer,在错误回调中的代码,前半部分是获取错误的堆栈进行打印的逻辑,后面是对各类错误的通用处理。

public void onError(Throwable e) {
        if (BuildConfig.DEBUG) {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stacks = e.getStackTrace();
            sb.append(e.getMessage());
            sb.append("\n");
            for (StackTraceElement stack : stacks) {
                sb.append(stack.getMethodName());
                sb.append("(");
                sb.append(stack.getClassName());
                sb.append(".java:");
                sb.append(stack.getLineNumber());
                sb.append(")");
                sb.append("\n");
            }
            LogUtils.e("Retrofit", sb.toString());
        }
        mBaseImpl.dismissProgress();
        if (e instanceof HttpException) {                 //   HTTP错误
            onException(ExceptionReason.BAD_NETWORK);
        } else if (e instanceof ConnectException
                || e instanceof UnknownHostException) {   //   连接错误
            onException(ExceptionReason.CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) { //  连接超时
            onException(ExceptionReason.CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {         //  解析错误
            onException(ExceptionReason.PARSE_ERROR);
        } else {
            onException(ExceptionReason.UNKNOWN_ERROR);
        }
    }

Observer中另一个最重要的结果回调onNext中对errcode进行过滤,因为我自己封装的model层返回的BaseResponce是没有errorCode的,这个model后面会讲到,当然我也可以自己给通用BaseResponse加上200的code但是总感觉这两个逻辑还是不要耦合的好,万一code变了我model也要改,所以我在我在BaseResponce中设置了一个变量fromCache用于标记返回结果为缓存。代码如下:


    @Override
    public void onNext(@NonNull T tBaseResponce) {
        LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
        if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
            onSuccess(tBaseResponce);
        } else {
            onFail(tBaseResponce);
        }
    }
public class BaseResponse<T>{

    @SerializedName("code")
    public int errCode;

    @SerializedName("msg")
    public String errMsg;

    @SerializedName("data")
    public T realData;

    /**
     * 請求結果是否來自緩存
     */
    public boolean fromCache = false;

    public BaseResponse<T> setData(T data){
        realData = data;
        return this;
    }

    @Override
    public String toString() {
        return "BaseResponse{" +
                "errCode='" + errCode + '\'' +
                ", errMsg='" + errMsg + '\'' +
                ", data=" + realData +
                '}';
    }
}

另外BaseObserver引用的BaseImpl是activity的抽象接口,托管了进度条和绑定了activity的生命周期的逻辑。

public abstract class BaseObserver<T extends BaseResponse> implements Observer<T> {

    private BaseImpl mBaseImpl;
    //  Activity 是否在执行onStop()时取消订阅
    private boolean isAddInStop = false;
    private boolean needProgress = false;

    public BaseObserver(BaseImpl mBaseImpl,boolean needProgress) {
        this.needProgress = needProgress;
        this.mBaseImpl = mBaseImpl;
    }

    @Override
    public void onSubscribe(@NonNull Disposable d) {
        if(needProgress) mBaseImpl.showProgress("加载中");
        if (isAddInStop) {    //  在onStop中取消订阅
            mBaseImpl.addRxStop(d);
        } else { //  在onDestroy中取消订阅
            mBaseImpl.addRxDestroy(d);
        }
    }

    @Override
    public void onNext(@NonNull T tBaseResponce) {
        LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
        if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
            onSuccess(tBaseResponce);
        } else {
            onFail(tBaseResponce);
        }
    }

    @Override
    public void onError(Throwable e) {
        if (BuildConfig.DEBUG) {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stacks = e.getStackTrace();
            sb.append(e.getMessage());
            sb.append("\n");
            for (StackTraceElement stack : stacks) {
                sb.append(stack.getMethodName());
                sb.append("(");
                sb.append(stack.getClassName());
                sb.append(".java:");
                sb.append(stack.getLineNumber());
                sb.append(")");
                sb.append("\n");
            }
            LogUtils.e("Retrofit", sb.toString());
        }
        mBaseImpl.dismissProgress();
        if (e instanceof HttpException) {                 //   HTTP错误
            onException(ExceptionReason.BAD_NETWORK);
        } else if (e instanceof ConnectException
                || e instanceof UnknownHostException) {   //   连接错误
            onException(ExceptionReason.CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) { //  连接超时
            onException(ExceptionReason.CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {         //  解析错误
            onException(ExceptionReason.PARSE_ERROR);
        } else {
            onException(ExceptionReason.UNKNOWN_ERROR);
        }
    }

    @Override
    public void onComplete() {
        if(needProgress) mBaseImpl.dismissProgress();
    }

    /**
     * 请求成功
     *
     * @param response 服务器返回的数据
     */
    abstract public void onSuccess(T response);

    /**
     * 服务器返回数据,但响应码不为200
     *
     * @param response 服务器返回的数据
     */
    public void onFail(T response) {
        String message = response.errMsg;
        if (TextUtils.isEmpty(message)) {
            ToastUtils.showShort(R.string.response_return_error);
        } else {
            ToastUtils.showShort(message);
        }
    }

    /**
     * 请求异常
     *
     * @param reason
     */
    public void onException(ExceptionReason reason) {
        switch (reason) {
            case CONNECT_ERROR:
                ToastUtils.showShort(R.string.connect_error, Toast.LENGTH_SHORT);
                break;

            case CONNECT_TIMEOUT:
                ToastUtils.showShort(R.string.connect_timeout, Toast.LENGTH_SHORT);
                break;

            case BAD_NETWORK:
                ToastUtils.showShort(R.string.bad_network, Toast.LENGTH_SHORT);
                break;

            case PARSE_ERROR:
                ToastUtils.showShort(R.string.parse_error, Toast.LENGTH_SHORT);
                break;

            case UNKNOWN_ERROR:
            default:
                ToastUtils.showShort(R.string.unknown_error, Toast.LENGTH_SHORT);
                break;
        }
    }

    /**
     * 请求网络失败原因
     */
    public enum ExceptionReason {
        /**
         * 解析数据失败
         */
        PARSE_ERROR,
        /**
         * 网络问题
         */
        BAD_NETWORK,
        /**
         * 连接错误
         */
        CONNECT_ERROR,
        /**
         * 连接超时
         */
        CONNECT_TIMEOUT,
        /**
         * 未知错误
         */
        UNKNOWN_ERROR,
    }
}
  • Model层的封装

逻辑流程图

然后说下上面提到的model层,我定义了接口IRepository。

这个model的主要逻辑是 :

首先判断是否需要强制刷新,如果不需要强制刷新则去数据库缓存中查看是否含有对象的缓存,如果是网络获取判断是否需要缓存。这里的逻辑主要由客户端控制。

public interface IRepository<T> {

    /**
     * 用于gson解析,以及一些Logname的打印。
     * @return
     */
    Class getTClass();

    Observable<T> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh);

    Observable<T> getEntry(final String url, Map<String, String> queryMap);

    T getCache(String url) throws Exception;

    Observable<T> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache);

    void saveCache(String url, T baseBeanList);

    String getCacheKey(String url, Map<String, String> queryMap);

    void clearCache();

}

model的实现

拿目前公司的restful接口数据格式类型举例:

{
    "code":200,
    "msg":"请求成功",
    "data":{
        "count":10,
        "game_list":[
            {
                "gameid":362938
            }
        ]
    }
}

可以看出BaseResponce返回的泛型T对应的data数据还需要继续解析。所以以目前的IRepository<T>

是代码是不能很好的封装满足需求的,所以我定义了抽象类继承IRepository。

DBListRepository<Container,RealTableBean extends BaseModel> implements IRepository<BaseResponse<Container>>

定义了2个泛型Cotainer和RealTableBean,数据库的相关操作基本由RealTableBean泛型实例完成,网络层的解析由Container完成,各司其职。GameContainer对应的是上图json的data,gameList对应的是上图json的game_list。当然如果有其他类型的restful结构,我只需要在定义对应类型的repository抽象类就好了,毕竟现在返回的restful接口的json格式非常局限满世界也就那么几种,所以不用担心repository的扩展类太多的问题。

而真正的Repository实例代码非常少,只需要继承4个接口就能满足上述定义的model接口的功能,如下:

public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {
    //用于Gson对泛型的解析
    @Override
    public Class getTClass() {
        return GameContainerBean.class;
    }

    //用于DB抽象类获取对数据库的引用
    @Override
    public Class getTableClass() {
        return GameContainerBean.GameListBean.class;
    }

    @Override
    public List<GameContainerBean.GameListBean> mapContainer(GameContainerBean beanContainer) {
        return beanContainer.gameList;
    }

    @Override
    public GameContainerBean mapTableBean(List<GameContainerBean.GameListBean> gameListBeen) {
        return new GameContainerBean(gameListBeen);
    }

}

BaseModel是我对实体的抽象继承的是DBflow的BaseModel可以进行数据库的增删改,很方便。

public abstract class BaseModel extends com.raizlabs.android.dbflow.structure.BaseModel{
    public static final String KEY = "keyUrl";
    @Column(name = KEY)
    public String keyUrl;
}

其中设置了一个通用参数keyUrl,主要用做对应的请求数据缓存查询。

其中key是由Url拼接上queryMap的参数组成,逻辑如下:

public String getCacheKey(String url, Map<String, String> queryMap) {
        StringBuilder sb = new StringBuilder();
        sb.append(url);
        if (queryMap != null && !queryMap.isEmpty()) {
            Set<String> keys = queryMap.keySet();
            sb.append("?");
            for (String key : keys) {
                sb.append(key).append("=").append(queryMap.get(key));
            }
        }
        return sb.toString();
    }

有个小问题,因为网络数据获取是从我们定义的retrofit通用接口中返回,返回的对象是Obserable<ResponseBody>而我们的model接受的参数是Observable<BaseResponce<T>>,等于是承包了GsonConvertFactory的工作,我们把返回的Observer通过rxjava的map转成我们的Model对应的的Observer类型就行了。

    @Override
    public Observable<BaseResponse<Container>> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache) {
        return HttpRequestFactory.retrofit().create(ApiService.class)
                .executeGet(url,queryMap).map(new Function<ResponseBody, BaseResponse<Container>>() {
                    @Override
                    public BaseResponse<Container> apply(@NonNull ResponseBody responseBody) throws Exception {
                        return GsonUtils.getInstance().fromJson(responseBody.string(), GsonUtils.type(BaseResponse.class,getTClass()));
                    }
                });
    }

 获取model集合的的主要逻辑代码块如下:

@Override
    public Observable<BaseResponse<Container>> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh) {

        final String key = getCacheKey(url, queryMap);

        //get cache
        Observable<BaseResponse<Container>> fromCache = Observable.create(new ObservableOnSubscribe<BaseResponse<Container>>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<BaseResponse<Container>> e) throws Exception {
                final BaseResponse<Container> cacheResponce = getCache(key);
                if (cacheResponce != null) {
                    LogUtils.e("Cache hint  | key = " + key);
                    cacheResponce.fromCache = true;
                    e.onNext(cacheResponce);
                } else {
                    e.onComplete();
                }
            }
        });

        //save cache
        Observable<BaseResponse<Container>> fromNet = getEntryFromNet(url, queryMap ,needCache).map(new Function<BaseResponse<Container>, BaseResponse<Container>>() {
            @Override
            public BaseResponse<Container> apply(@NonNull BaseResponse<Container> tBaseResponse) throws Exception {
                if (needCache) saveCache(key, tBaseResponse);
                return tBaseResponse;
            }
        });

        if (forceRefresh) {
            return fromNet;
        }
        return Observable.concat(fromCache, fromNet)
                .subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

可以看到,Cache命中的时候会将里面BaseResponce的fromCache标记为true。这样就能和上面的提到的

BaseObserver对应的onNext逻辑相吻合了。

  • 测试实例

最后调用数据的例子如下:

new GameBeanRespository()
                .getEntry(new UrlConstant.Builder(false).shuffix().game().list().build()//参数url
                        ,new RxMap()
                                .put("page","2")
                                .put("offset","10")
                                .build())//参数query maps
                .subscribe(new BaseObserver<BaseResponse<GameContainerBean>>(this,false) {
                    @Override
                    public void onSuccess(BaseResponse<GameContainerBean> response) {
                        LogUtils.d(response.realData);
                    }
                });

TestRepository继承于DBRepository主要实现了getCache和getFromNet部分

上面看到的rxMap只是我写的一个链式调用的Map包装类,链式调用编写的效率和心情大家应该都理解 -3-

有兴趣的可以拿去用,也就是个小玩意儿。

public class RxMap<T,R>{

    Map<T,R> map;

    public static <T,R> RxMap<T,R> newInstance(){
        return new RxMap<>();
    }

    public RxMap() {
        this.map = new HashMap<>();
    }

    public RxMap(Map<T,R> map) {
        this.map = map;
    }

    public RxMap<T,R> put(T t, R r){
        map.put(t,r);
        return this;
    }

    public Map<T,R> build(){
        return map;
    }
}

DBflow

简单的说下DBflow,可能你直接看到了bean的实例进行了数据库的save操作,觉得很酸爽,确实很酸爽,而且DBflow继承了GreenDao和OrmLite各自的优点,简单易用上无可挑剔,自动生成数据Dao类,只需要类似于OrmLite利用注解声明各个bean之间的关系,另外继承BaseModel就让bean自己具备了增删改的能力了。

关于DBflow这个数据库的使用我就不多说了,因为太简单,学习成本低,推荐大家去用,用了感觉不爽来打我 - -!!,当然我不会告诉你我在哪里上班的。

后续我会抽出一个框架的demo的github地址补充在文章下面。

© 著作权归作者所有

yizhihaohut
粉丝 7
博文 21
码字总数 19843
作品 0
松江
程序员
私信 提问
一套整合主流HTTP网络、图片加载、MVP(RxJava2+Dagger2)架构的快速高效的开发框架RxEasyAndroid

需要的环境 JDK1.7 SDK AndroidStudio开发工具 特性 整合主流HTTP网络(Retrofit2、OKHTTP3)、图片加载(Glide)、MVP(RxJava2+Dagger2)架构的一套快速高效的开发框架 包含app library 两...

wu928320442
2017/10/23
0
0
RxEasyAndroid 2.1发布

Github地址 https://github.com/wu928320442/RxEasyAndroid 需要的环境 JDK1.7 SDK AndroidStudio开发工具 特性 整合主流HTTP网络(Retrofit2、OKHTTP3)、图片加载(Glide)、MVP(RxJava2...

wu928320442
2017/11/02
0
0
Android实战——RxJava2解锁图片三级缓存框架

RxJava2解锁图片三级缓存框架 本篇文章包括以下内容 前言 图片三级缓存的介绍 框架结构目录的介绍 构建项目整体框架 实现图片三级缓存 演示效果 源码下载 结语 前言 RxJava2作为如今那么主流...

qq_30379689
2017/04/07
0
0
Android实战——RxJava2+Retrofit+RxBinding解锁各种新姿势

RxJava2+Retrofit+RxBinding解锁各种新姿势 本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。 本篇文章内容包含以下内容 前言 RxJava2的基本介绍 RxJava2观察者模式的介绍 RxJava2观...

qq_30379689
2017/04/03
0
0
Android RxJava 实际应用讲解:从磁盘 / 内存缓存中 获取缓存数据

前言 ,由于其基于事件流的链式调用、逻辑简洁 & 使用简单的特点,深受各大 开发者的欢迎。 Github截图 如果还不了解RxJava,请看文章:Android:这是一篇 清晰 & 易懂的Rxjava 入门教程 如此...

Carson_Ho
2017/11/06
0
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周四乱弹 —— 当你简历注水但还是找到了工作

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @花间小酌 :#今日歌曲推荐# 分享成龙的单曲《男儿当自强》。 《男儿当自强》- 成龙 手机党少年们想听歌,请使劲儿戳(这里) @hxg2016 :刚在...

小小编辑
今天
2.7K
22
靠写代码赚钱的一些门路

作者 @mezod 译者 @josephchang10 如今,通过自己的代码去赚钱变得越来越简单,不过对很多人来说依然还是很难,因为他们不知道有哪些门路。 今天给大家分享一个精彩的 GitHub 库,这个库整理...

高级农民工
昨天
4
0
用好项目管理工具,人人都可以成为项目经理

现在市面上的项目管理工具越来越多了,但是大多数都是一些协同工具或轻量项目管理工具。如果是多团队、跨部门使用或者企业级的项目管理,从管理思想到工具运用,需要适应企业的业务流程体系,...

cs平台
昨天
12
0
只需一步,在Spring Boot中统一Restful API返回值格式与统一处理异常

统一返回值 在前后端分离大行其道的今天,有一个统一的返回值格式不仅能使我们的接口看起来更漂亮,而且还可以使前端可以统一处理很多东西,避免很多问题的产生。 比较通用的返回值格式如下:...

晓月寒丶
昨天
69
0
区块链应用到供应链上的好处和实际案例

区块链可以解决供应链中的很多问题,例如记录以及追踪产品。那么使用区块链应用到各产品供应链上到底有什么好处?猎头悬赏平台解优人才网小编给大家做个简单的分享: 使用区块链的最突出的优...

猎头悬赏平台
昨天
32
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部