<转>RxJava+Retrofit+OkHttp深入浅出-终极封装五(数据持久化)

背景

数据持久化在现在移动app开发中已经越来越被大家认可,提高了用户体验和软件的稳定性,但是由于retrofit持久化的局限性,所以需要自己动手改造一个适合自己的数据持久化方案!

效果

20161101085509197.gif

第一次请求是网络加载,之后只要在设置的保鲜时间以内都是通过缓存拉取数据,提高加载速度!

下面我们分两节讲解,一节讲述自带的retrofit-cache用法和缺陷,一节讲述自己定义的缓存处理方案

Retrofit-cookie

由于retrofit是基于okhttp的,所以他的cache原理就是运用了okhttp的cookie处理;

注意:这里自带的cookie前提是服务器提供了支持(返回头有cache信息),只有get请求才具备http的缓存功能,post没有!没有!没有

Retrofit-Cache的内容

1.http缓存相关头:

Expires (实体标头,HTTP 1.0+):一个GMT时间,试图告知客户端,在此日期内,可以信任并使用对应缓存中的副本,缺点是,一但客户端日期不准确.则可能导致失效

2.Pragma : no-cache(常规标头,http1.0+)
3.Cache-Control : (常规标头,HTTP1.1)
3.1public:(仅为响应标头)响应:告知任何途径的缓存者,可以无条件的缓存该响应
3.2private(仅为响应标头):响应:告知缓存者(据我所知,是指用户代理,常见浏览器的本地缓存.用户也是指,系统用户.但也许,不应排除,某些网关,可以识别每个终端用户的情况),只针对单个用户缓存响应. 且可以具体指定某个字段.如private –“username”,则响应头中,名为username的标头内容,不会被共享缓存.
3.3no-cache:告知缓存者,必须原原本本的转发原始请求,并告知任何缓存者,别直接拿你缓存的副本,糊弄人.你需要去转发我的请求,并验证你的缓存(如果有的话).对应名词:端对端重载.

cache-retrofit使用

注解使用,具体方法具体设置(max-age设置的是保鲜时间)

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();

当然我们肯定想要动态设置,而且每一个get方法都需要缓存保鲜处理,怎么解决呢?

1.开辟一片本地空间,设置给OkHttpClient.Builder

OkHttpClient.Builder builder = new OkHttpClient.Builder();
    /*缓存位置和大小*/
  builder.cache(new Cache(MyApplication.app.getCacheDir(),10*1024*1024));

2.设置拦截器,请求前判断网络,拦截数据和返回本地数据

网上很多资源都是错误的,走了很多弯路,注意这里一定要返回一个新的Response 不让不会有结果显示

/**
 * get缓存方式拦截器
 * Created by WZG on 2016/10/26.
 */

public class CacheInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        if (!isNetworkAvailable(MyApplication.app)) {//没网强制从缓存读取(必须得写,不然断网状态下,退出应用,或者等待一分钟后,就获取不到缓存)
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
        }
        Response response = chain.proceed(request);
        Response responseLatest;
        if (isNetworkAvailable(MyApplication.app)) {
            int maxAge = 60; //有网失效一分钟
            responseLatest = response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .build();
        } else {
            int maxStale = 60 * 60 * 6; // 没网失效6小时
            responseLatest= response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .build();
        }
        return  responseLatest;
    }
}

有网情况下,一分钟内访问的请求不会去真正http请求,而是从cache中获取;
没网情况下,一律从缓存获取,6小时过期时间。

3.设置OkHttpClient.Builder设置拦截器

addNetworkInterceptor在请求发生前和发生后都处理一遍,addInterceptor在有结果返回后处理一遍
注意:这里一定要两个方法同时设置才能保证生效,暂时没搞懂为什么

     OkHttpClient.Builder builder = new OkHttpClient.Builder();
    builder.addNetworkInterceptor(new CacheInterceptor());
    builder.addInterceptor(new CacheInterceptor());

现在你的retrofit就能自动给get添加cookie了!

总结

自带数据持久化处理方便快捷简单,但是局限性太大,必须是get请求而且还需要服务器配合头文件返回处理,所以在实际开发中并不适用;所以才有了自定义cookie处理的方案

自定义本地数据持久化方案

思路

主要是通过greenDao数据库存放数据,在网络请求成功后保存数据,再次请求判断url是否已经存在缓存数据
有网络:onstart中判断再判断保鲜时间,如果有效返回缓存数据,无效则再一次请求数据!
无网络(包含各种失败):onError中判断处理,有效时间内返回数据,无效自定义的网络错误抛出异常!

1.创建缓存对象数据

记录返回数据,标识url,和缓存时间
/**

  • post請求緩存数据
  • Created by WZG on 2016/10/26.
    /
    @Entity
    public class CookieResulte {
    @Id
    private long id;
    /
    url/
    private String url;
    /
    返回结果/
    private String resulte;
    /
    时间*/
    private long time;
    }
2.BaseApi添加缓存相关设置参数

保持和封装1-4封装的一致性,将缓存的相关设置放入在BaseApi中,并且将baseUrl和超时connectionTime也包含进来,更加灵活

/**
 * 请求数据统一封装类
 * Created by WZG on 2016/7/16.
 */
public abstract class BaseApi<T> implements Func1<BaseResultEntity<T>, T> {
    //rx生命周期管理
    private SoftReference<RxAppCompatActivity> rxAppCompatActivity;
    /*回调*/
    private SoftReference<HttpOnNextListener> listener;
    /*是否能取消加载框*/
    private boolean cancel;
    /*是否显示加载框*/
    private boolean showProgress;
    /*是否需要缓存处理*/
    private boolean cache;
    /*基础url*/
    private  String baseUrl="http://www.izaodao.com/Api/";
    /*方法-如果需要缓存必须设置这个参数;不需要不用設置*/
    private String mothed;
    /*超时时间-默认6秒*/
    private int connectionTime = 6;
    /*有网情况下的本地缓存时间默认60秒*/
    private int cookieNetWorkTime=60;
    /*无网络的情况下本地缓存时间默认30天*/
    private int cookieNoNetWorkTime=24*60*60*30;
}

注意:如果需要使用缓存功能必须要设置mothed参数(和baseurl拼成一个url标识缓存数据)

3.拦截Gson数据

由于使用GsonConverterFactory自动解析数据,所以需要在自动转换前得到服务器返回的数据,我们可以自定义Interceptor在addInterceptor(成功后调用)拦截数据,保存到本地数据库中!

/**
 * gson持久化截取保存数据
 * Created by WZG on 2016/10/20.
 */
public class CookieInterceptor implements Interceptor {
    private CookieDbUtil dbUtil;
    /*是否缓存标识*/
    private boolean cache;

    public CookieInterceptor( boolean cache) {
        dbUtil=CookieDbUtil.getInstance();
        this.cache=cache;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);
        if(cache){
            ResponseBody body = response.body();
            BufferedSource source = body.source();
            source.request(Long.MAX_VALUE); // Buffer the entire body.
            Buffer buffer = source.buffer();
            Charset charset = Charset.defaultCharset();
            MediaType contentType = body.contentType();
            if (contentType != null) {
                charset = contentType.charset(charset);
            }
            String bodyString = buffer.clone().readString(charset);
            String url = request.url().toString();
            CookieResulte resulte= dbUtil.queryCookieBy(url);
            long time=System.currentTimeMillis();
            /*保存和更新本地数据*/
            if(resulte==null){
                resulte  =new CookieResulte(url,bodyString,time);
                dbUtil.saveCookie(resulte);
            }else{
                resulte.setResulte(bodyString);
                resulte.setTime(time);
                dbUtil.updateCookie(resulte);
            }
        }
        return response;
    }
}
4.添加回调方法

因为缓存回调过程中无法手动传递Gson对象,也就是ResulteEntity中的T泛型,所以自由单独添加一个方法,返回缓存数据!考虑到可能不需要回到所以写成了具体的方法,可主动覆盖!

/**
 * 成功回调处理
 * Created by WZG on 2016/7/16.
 */
public abstract class HttpOnNextListener<T> {
    /**
     * 成功后回调方法
     * @param t
     */
    public abstract void onNext(T t);

    /**
     * 緩存回調結果
     * @param string
     */
    public void onCacheNext(String string){

    }
    *********
}
5.数据持久化调用,获取缓存

这里分两种情况,有网络-和无网络(包含各种失败不单单只是无网络)

有网

判断是否存在缓存,如果有判断保鲜时间,有效期内返回数据,失效在一起请求;

 /**
     * 订阅开始时调用
     * 显示ProgressDialog
     */
    @Override
    public void onStart() {
        showProgressDialog();
        /*缓存并且有网*/
        if(api.isCache()&& AppUtil.isNetworkAvailable(MyApplication.app)){
             /*获取缓存数据*/
            CookieResulte cookieResulte= CookieDbUtil.getInstance().queryCookieBy(api.getUrl());
            if(cookieResulte!=null){
                long time= (System.currentTimeMillis()-cookieResulte.getTime())/1000;
                if(time< api.getCookieNetWorkTime()){
                    if( mSubscriberOnNextListener.get()!=null){
                        mSubscriberOnNextListener.get().onCacheNext(cookieResulte.getResulte());
                    }
                    onCompleted();
                    unsubscribe();
                }
            }
        }
    }
无网络(失败情况)

原理和有网络一样,但是额外的加入了rx异常处理,防止用户在处理工程中导致错误崩溃!并且无缓冲抛出自定义异常

    /**
     * 对错误进行统一处理
     * 隐藏ProgressDialog
     *
     * @param e
     */
    @Override
    public void onError(Throwable e) {
        dismissProgressDialog();
        /*需要緩存并且本地有缓存才返回*/
        if(api.isCache()){
            Observable.just(api.getUrl()).subscribe(new Subscriber<String>() {
                @Override
                public void onCompleted() {

                }

                @Override
                public void onError(Throwable e) {
                    errorDo(e);
                }

                @Override
                public void onNext(String s) {
                    /*获取缓存数据*/
                    CookieResulte cookieResulte= CookieDbUtil.getInstance().queryCookieBy(s);
                    if(cookieResulte==null){
                        throw new HttpTimeException("网络错误");
                    }
                    long time= (System.currentTimeMillis()-cookieResulte.getTime())/1000;
                    if(time<api.getCookieNoNetWorkTime()){
                        if( mSubscriberOnNextListener.get()!=null){
                            mSubscriberOnNextListener.get().onCacheNext(cookieResulte.getResulte());
                        }
                    }else{
                        CookieDbUtil.getInstance().deleteCookie(cookieResulte);
                        throw new HttpTimeException("网络错误");
                    }
                }
            });
        }else{
            errorDo(e);
        }
    }
6.回调解析数据

由于是返回的string数据,所以需要在回调onCacheNext中手动解析Gson数据


    //   回调一一对应
    HttpOnNextListener simpleOnNextListener = new HttpOnNextListener<List<SubjectResulte>>() {
        @Override
        public void onNext(List<SubjectResulte> subjects) {
            tvMsg.setText("网络返回:\n" + subjects.toString());
        }

        @Override
        public void onCacheNext(String cache) {
            /*缓存回调*/
            Gson gson=new Gson();
            java.lang.reflect.Type type = new TypeToken<BaseResultEntity<List<SubjectResulte>>>() {}.getType();
            BaseResultEntity resultEntity= gson.fromJson(cache, type);
            tvMsg.setText("缓存返回:\n"+resultEntity.getData().toString() );
        }
    };

好了,一套自定义的缓存方案就解决了!

总结

优点:

1.有效的解决了post请求缓存的问题
2.可以同时缓存get数据
3.自定义更加灵活,可更换任意第三方库

缺点:

1.缓存数据无法和onext公用一个回到接口,导致需要手动解析数据(由于Gson自动转换导致)

由于Gson在回调的过程中和使用过程中给程序导致的一些列的限制,所以决定封装一个变种框架,去掉Gson自动解析回调功能,改用String回调,让回调接口一对多处理,并且解决缓存无法和成功统一回调的问题!欢迎大家关注!

如有帮助换start和follow!

终极封装专栏

RxJava+Retrofit+OkHttp深入浅出-终极封装专栏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容