自己封装一个okhttp,一个看的懂的okhttp封装

前言:封装只是加深自己的理解,网上已经有很优秀的封装,我也是借鉴了okgo和鸿洋的okhttputils。本项目是基于mvc模式下,但这篇只讲如何对okhttp进行封装(这里我按最基础步骤来,需要额外功能,看源码和本文理解,肯定可以实现)。

我们封装要有的功能有:

  • 支持get请求
  • 支持post请求
  • 支持上传文件
  • 支持下载文件和断点续传
  • 有网络时,支持缓存(连接网络时的有效期)
  • 断开网络,支持离线缓存(离线缓存有效期)
  • 多次请求同一url,在网络还在请求时,是否只请求一次
  • 支持请求失败,自动重连

先看看效果展示 (建议打开权限)

 

 

首先okhttp进行简单get请求代码是这样的:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .build();
        Request.Builder mBuilder = new Request.Builder();
        mBuilder.url("url?parm1=x&parm2=y");
        mBuilder.header("head","headValue");
        Request okHttpRequest = mBuilder.build();
        okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

            }
        });

get请求参数是拼在url后面的,而且上面是异步请求enqueue,这个方法是在子线程里的。回调onFailure和onResponse也都在子线程,我们可以把解析等耗时操作放在这里,但是这里不能直接更改UI,要把他回调到主线程里。

1、封装EasyOk

看到上面简单的网络请求,okHttpClient不可能一直new,而且在网络请求的时候要有取消网络请求,取消网络请求代码如下

//tag取消网络请求
    public void cancleOkhttpTag(String tag) {
        Dispatcher dispatcher = okHttpClient.dispatcher();
        synchronized (dispatcher) {
            //请求列表里的,取消网络请求
            for (Call call : dispatcher.queuedCalls()) {
                if (tag.equals(call.request().tag())) {
                    call.cancel();
                }
            }
            //正在请求网络的,取消网络请求
            for (Call call : dispatcher.runningCalls()) {
                if (tag.equals(call.request().tag())) {
                    call.cancel();
                }
            }
        }
    }

可以看到,取消代码请求要用的okHttpClient,所以我们要保持okHttpClient的唯一性,这里就要用到单例了。所以EasyOk最开始是这样的:

public class EasyOk {
    private static EasyOk okHttpUtils;
    private OkHttpClient okHttpClient;
    //这个handler的作用是把子线程切换主线程。在后面接口中的具体实现,就不需要用handler去回调了
    private Handler mDelivery;
    private EasyOk() {
        mDelivery = new Handler(Looper.getMainLooper());
        okHttpClient = new OkHttpClient.Builder()
                .hostnameVerifier(new HostnameVerifier() {//证书信任
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        return true;
                    }
                })
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .build();
    }


    public static EasyOk getInstance() {
        if (okHttpUtils == null) {
            okHttpUtils = new EasyOk();
        }
        return okHttpUtils;
    }


    public OkHttpClient getOkHttpClient() {
        return okHttpClient;
    }

    public Handler getmDelivery() {
        return mDelivery;
    }


    //tag取消网络请求
    public void cancleOkhttpTag(String tag) {
        Dispatcher dispatcher = okHttpClient.dispatcher();
        synchronized (dispatcher) {
            //请求列表里的,取消网络请求
            for (Call call : dispatcher.queuedCalls()) {
                if (tag.equals(call.request().tag())) {
                    call.cancel();
                }
            }
            //正在请求网络的,取消网络请求
            for (Call call : dispatcher.runningCalls()) {
                if (tag.equals(call.request().tag())) {
                    call.cancel();
                }
            }
        }
    }

}

mDelivery是将子线程切换到主线程的handler,这里借鉴了鸿洋大神的思路。因为我们最好把重复性的工作全部都放在封装里。封装最重要的优点不就是为了方便吗。

根据最原始的okhttp进行的get请求,我们还缺Request,还有一个网络请求的回调,接下来是:

2、封装OkGetBuilder(这里我将每种请求封装成了不同的Builder,虽然重复了好多操作,但更加清晰,偏于理解)

因为每次请求,Request 请求体都是需要new的,所以可想而知这里不可能是单例,而且每次调用请求都是new出来的Request。根据最原始的get请求,我们知道OkGetBuilder里需要1、url,2、参数,3、header,4、tag,5、还有自己的网络回调。

所以我们得用有个网络回调接口,这里我用的是抽象类ResultMyCall,这里用抽象类的好处是,我们可以把统一重复操作放在父类里,只要不重写方法,都会按父类方法去实现,所以这里有时候你只需要重写一个onSuccess方法即可,不像接口一样要把方法全部实现。要注意的是,如果要基于mvc,最好用接口ResulCall,这块到时候介绍mvc的时候回介绍。现在我们都按抽象类ResultMyCall走。抽象类如下ResultMyCall

public abstract class ResultMyCall<T> {

    //请求网络之前,一般展示loading
    public void onBefore() {
        
    }

    //请求网络结束,消失loading
    public void onAfter() {

    }

    //监听上传图片的进度(目前支持图片上传,其他重写这个方法无效)
    public void inProgress(float progress) {

    }


    //错误信息
    public void onError(String errorMessage) {
        ToastUtils.showToast(errorMessage);
    }

    public void onSuccess(Object response) {
        
    }

    //如果带了泛型T,这里个方法会获取泛型的type,用于解析,如果不带泛型,默认返回的是String
    public Type getSuperclassTypeParameter(Class<?> subclass) {
        Type superclass = subclass.getGenericSuperclass();
        if (superclass instanceof Class) {
            return null;
        }
        ParameterizedType parameterized = (ParameterizedType) superclass;
        return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
    }

    public Type getType() {
        return getSuperclassTypeParameter(getClass());
    }


}

相信这个类也很容易理解,这里的OnError我写了一个Toast。所以每次请求的回调onError可以不重写,会自动弹Toast,如果你需要有其他操作,比如说不弹Toast,是需要打开另外一个页面则可以重写这个方法如:

            @Override
            public void onError(String errorMessage) {
                super.onError(errorMessage);
                //注释super.onError(errorMessage);那么不会走父类方法,

            }

 

那么简单OkGetBuilder如下:

public class OkGetBuilder {
    
    private String url;
    private String tag;
    private Map<String, String> headers;
    private Map<String, String> params;

    
    private OkHttpClient okHttpClient;
    private Context context;
    private Handler mDelivery;
    private Request okHttpRequest;

    public OkGetBuilder() {
        this.okHttpClient = EasyOk.getInstance().getOkHttpClient();
        this.context = MyApplication.getContext();
        this.mDelivery = EasyOk.getInstance().getmDelivery();
    }


    public OkGetBuilder build() {
        Request.Builder mBuilder = new Request.Builder();
        if (params != null) {
            mBuilder.url(appendParams(url, params));
        } else {
            LogUtils.i("网络请求", "请求接口 ==>> " + url);
            mBuilder.url(url);
        }

        if (!TextUtils.isEmpty(tag)) {
            mBuilder.tag(tag);
        }

        if (headers != null) {
            mBuilder.headers(appendHeaders(headers));
        }
        okHttpRequest = mBuilder.build();
        return this;
    }


    public void enqueue(final ResultMyCall resultMyCall) {
        if (resultMyCall != null) {
            mDelivery.post(new Runnable() {
                @Override
                public void run() {
                    resultMyCall.onBefore();
                }
            });
        }

        okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                if (resultMyCall != null) {
                    mDelivery.post(new Runnable() {
                        @Override
                        public void run() {
                            resultMyCall.onAfter();
                            String errorMsg;
                            if (e instanceof SocketException) {

                            } else {
                                if (e instanceof ConnectException) {
                                    errorMsg = context.getString(R.string.network_unknow);
                                } else if (e instanceof SocketTimeoutException) {
                                    errorMsg = context.getString(R.string.network_overtime);
                                } else {
                                    errorMsg = context.getString(R.string.server_error);
                                }
                                resultMyCall.onError(errorMsg);
                            }


                        }
                    });

                }
            }

            @Override
            public void onResponse(Call call, final Response response) throws IOException {
                //网络请求成功
                if (response.isSuccessful()) {
                    if (resultMyCall != null) {
                        String result = response.body().string();
                        Object successObject = null;
                        try {
                            if (resultMyCall.getType() == null) {
                                successObject = result;
                            } else {
                                successObject = GsonUtil.deser(result, resultMyCall.getType());
                            }

                        } catch (Throwable e) {
                            mDelivery.post(new Runnable() {
                                @Override
                                public void run() {
                                    resultMyCall.onAfter();
                                    resultMyCall.onError("数据解析出错了");
                                }
                            });
                            return;
                        }

                        if (successObject == null) {
                            successObject = result;
                        }

                        final Object finalSuccessObject = successObject;
                        mDelivery.post(new Runnable() {
                            @Override
                            public void run() {
                                resultMyCall.onAfter();
                                resultMyCall.onSuccess(finalSuccessObject);
                            }
                        });

                    }
                } else {
                    //接口请求确实成功了,code 不是 200
                    if (resultMyCall != null) {
                        final String errorMsg = response.body().string();
                        mDelivery.post(new Runnable() {
                            @Override
                            public void run() {
                                resultMyCall.onAfter();
                                resultMyCall.onError(errorMsg);
                            }
                        });
                    }
                }

            }
        });
    }


    public OkGetBuilder url(String url) {
        this.url = url;
        return this;
    }

    public OkGetBuilder tag(String tag) {
        this.tag = tag;
        return this;
    }

    public OkGetBuilder headers(Map<String, String> headers) {
        this.headers = headers;
        return this;
    }

    public OkGetBuilder params(Map<String, String> params) {
        this.params = params;
        return this;
    }

    private Headers appendHeaders(Map<String, String> headers) {
        Headers.Builder headerBuilder = new Headers.Builder();
        if (headers == null || headers.isEmpty()) return null;

        for (String key : headers.keySet()) {
            headerBuilder.add(key, headers.get(key));
        }
        return headerBuilder.build();
    }

    //get 参数拼在url后面
    private String appendParams(String url, Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        if (url.indexOf("?") == -1) {
            sb.append(url + "?");
        } else {
            sb.append(url + "&");
        }

        if (params != null && !params.isEmpty()) {
            for (String key : params.keySet()) {
                sb.append(key).append("=").append(params.get(key)).append("&");
            }
        }
        sb = sb.deleteCharAt(sb.length() - 1);
        LogUtils.i("网络请求", "请求接口 ==>> " + sb.toString());
        return sb.toString();
    }


}

首先我们new这个类的时候把唯一的okHttpClient拿到用来进行请求,把mDelivery拿到,用于子线程切换主线程,那么在后面的回调方法里可以直接进行UI操作。OkGetBuilder里要做的是把heads用map传入,那么要进行一个循环add到head上去如:appendHeaders方法,参数也用map传入,拼接参数如:appendParams方法。

那么接下来就是请求这块:

1、在调用自定义方法enqueue(我这里和okhttp重名了偏于理解),首先我们这里就要回调onBefore,如:

resultMyCall.onBefore()

2、在原始回调失败onFailure里,当然是回调我们的onAfter和onError。

3、在原始回调onResponse里,这里会比较麻烦,即使返回code=200;这里还有2种情况,一种是正常的按你传入的泛型解析,一种是比如:点击关注,网络请求也成功了,但接口问题返回关注失败。我们公司用的是status为0代表失败,这里要特别注意

4、OkGetBuilder封装好了,把它放进EasyOk里如下:

public static OkGetBuilder get() {
        return new OkGetBuilder();
    }

 

3、最后封装后的进行GET请求用法如下:(其实post和上传文件都是这个思路,不同的是post有多种RequestBody,上传文件也是post的一种,这里具体可以看鸿洋和okgo的封装):

//这些是全部方法,没有用到的不使用
//paramsBuilder 是我封装用的一个传递参数的类。有要用的参数一致点下去就好了...
EasyOk.get().url("http://gank.io/api/xiandu/category/wow")
                .tag("cancleTag")
                //内部已经做了null处理,请求头部
                //.headers(paramsBuilder.getHeads())
                //内部已经做了null处理,请求参数
                //.params(paramsBuilder.getParams())
                .build().enqueue(new ResultMyCall<T>() {
            @Override
            public void onBefore() {
                super.onBefore();

            }

            @Override
            public void onAfter() {
                super.onAfter();

            }


            @Override
            public void onError(String errorMessage) {
                super.onError(errorMessage);

            }

            @Override
            public void onSuccess(Object response) {
                super.onSuccess(response);
               //如果你再new ResultMyCall的时候带了泛型,那么这里只需要
               //T bean = (T)response ;
               //如果没有带泛型,那么默认返回的string类型,
               //Sring bean = (String)response;
            }
        });

如果onBefore和onAfter还有onError,都把统一操作封装好了并且不需要重写没有特殊操作的你可以这样:


EasyOk.get().url("http://gank.io/api/xiandu/category/wow")
                .tag("cancleTag")
                .build().enqueue(new ResultMyCall<T>() {

                @Override
                public void onSuccess(Object response) {
                super.onSuccess(response);

               //如果你再new ResultMyCall的时候带了泛型,那么这里只需要
               //T bean = (T)response ;
               //如果没有带泛型,那么默认返回的string类型,
               //Sring bean = (String)response;

                }
            });

上面介绍我我把缓存和重连等其他功能没有说,因为加进去太复杂也不清晰,等后面单独拿出来讲,通过本文的理解,你会知道怎么去封装,加到什么地方去。

4、接下说的是下载文件及断点续传下载文件

经过上述介绍,咱们直接看OkDownloadBuilder里的内容:

public class OkDownloadBuilder {
    //断点续传的长度
    private long currentLength;
    private String url;
    private String tag;
    //文件路径(不包括文件名)
    private String path;
    //文件名
    private String fileName;

    //是否开启断点续传
    private boolean resume;
    //只允许一个在当前下载线程中
    private boolean onlyOneNet;

    /**
     * okHttpUtils里单例里唯一
     */
    private OkHttpClient okHttpClient;
    private Context context;
    private Handler mDelivery;

    /**
     * 每次请求网络生成的请求request
     */
    private Request.Builder mBuilder;

    public OkDownloadBuilder() {
        this.okHttpClient = EasyOk.getInstance().getOkHttpClient();
        this.context = MyApplication.getContext();
        this.mDelivery = EasyOk.getInstance().getmDelivery();
    }

    public OkDownloadBuilder build() {
        mBuilder = new Request.Builder();
        mBuilder.url(url);
        if (!TextUtils.isEmpty(tag)) {
            mBuilder.tag(tag);
        }
        //这里只要断点上传,总会走缓存。。所以强制网络下载
        mBuilder.cacheControl(CacheControl.FORCE_NETWORK);
        return this;
    }


    public void removeOnceTag() {
        if (onlyOneNet) {
            if (!TextUtils.isEmpty(tag)) {
                EasyOk.getInstance().getOnesTag().remove(tag);
            } else {
                EasyOk.getInstance().getOnesTag().remove(url);
            }
        }
    }

    public void enqueue(final OnDownloadListener listener) {
        if (onlyOneNet) {
            if (!TextUtils.isEmpty(tag)) {
                if (EasyOk.getInstance().getOnesTag().contains(tag)) {
                    return;
                }
                EasyOk.getInstance().getOnesTag().add(tag);
            } else {
                if (EasyOk.getInstance().getOnesTag().contains(url)) {
                    return;
                }
                EasyOk.getInstance().getOnesTag().add(url);
            }
        }

        if (resume) {
            File exFile = new File(path, fileName);
            if (exFile.exists()) {
                currentLength = exFile.length();
                mBuilder.header("RANGE", "bytes=" + currentLength + "-");
            }
        }
        Request okHttpRequest = mBuilder.build();
        okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                removeOnceTag();
                //下载失败监听回调
                mDelivery.post(new Runnable() {
                    @Override
                    public void run() {
                        listener.onDownloadFailed(e);
                    }
                });
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                removeOnceTag();
                InputStream is = null;
                byte[] buf = new byte[1024];
                int len = 0;
                FileOutputStream fos = null;

                //储存下载文件的目录
                File dir = new File(path);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                final File file = new File(dir, fileName);

                try {
                    is = response.body().byteStream();
                    //总长度
                    final long total;
                    //如果当前长度就等于要下载的长度,那么此文件就是下载好的文件
                    //前提是这里是默认下载的同意文件,要判断是否可以断点续传,最好在开启网络的时候判断是否是同意版本号
                    if (currentLength == response.body().contentLength()) {
                        mDelivery.post(new Runnable() {
                            @Override
                            public void run() {
                                listener.onDownloadSuccess(file);
                            }
                        });
                        return;
                    }
                    if (resume) {
                        total = response.body().contentLength() + currentLength;
                    } else {
                        total = response.body().contentLength();
                    }
                    mDelivery.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onDownLoadTotal(total);
                        }
                    });
                    if (resume) {
                        //这个方法是文件开始拼接
                        fos = new FileOutputStream(file, true);
                    } else {
                        //这个是不拼接,从头开始
                        fos = new FileOutputStream(file);
                    }
                    long sum;
                    if (resume) {
                        sum = currentLength;
                    } else {
                        sum = 0;
                    }
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                        sum += len;
                        final int progress = (int) (sum * 1.0f / total * 100);
                        //下载中更新进度条
                        mDelivery.post(new Runnable() {
                            @Override
                            public void run() {
                                listener.onDownloading(progress);
                            }
                        });

                    }
                    fos.flush();
                    //下载完成
                    mDelivery.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onDownloadSuccess(file);
                        }
                    });

                } catch (final Exception e) {
                    mDelivery.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onDownloadFailed(e);
                        }
                    });
                } finally {

                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {

                    }

                }


            }
        });

    }


    public OkDownloadBuilder onlyOneNet(boolean onlyOneNet) {
        this.onlyOneNet = onlyOneNet;
        return this;
    }


    public OkDownloadBuilder path(String path) {
        this.path = path;
        return this;
    }


    public OkDownloadBuilder fileName(String fileName) {
        this.fileName = fileName;
        return this;
    }


    public OkDownloadBuilder url(String url) {
        this.url = url;
        return this;
    }

    public OkDownloadBuilder tag(String tag) {
        this.tag = tag;
        return this;
    }

    public OkDownloadBuilder resume(boolean resume) {
        this.resume = resume;
        return this;
    }

如果你不看断点续传这块,在onResponse里其实就是输入流和文件流,把流写进文件里的操作;断点的续传的关键点是哪些?要知道断点续传其实就是接着上次未下载的文件继续下载(当然这里要确保下载的是同一文件,如下载更新要保证是同一版本号,这里在获得版本更新内容的时候判断是否是同一版本)。关键有2点,

1、在开启文件下载的时候,要在header里加上当前文件长度如:

mBuilder.header("RANGE", "bytes=" + currentLength + "-");

2、同时在文件流的时候要告诉流,我们不是覆盖,是拼接如:

fos = new FileOutputStream(file, true);//没错就是这个true

5、有网的时候的在线缓存

场景如下:假如首页广告,get请求下来的数据,而且这个可能1个星期才会换一次数据,这个时候如果没有这个功能,每次进首页都会去请求网络,如果有这功能,那么如果缓存内容在有效期就会跳过网络请求,直接取缓存。这样节约流量之余还能减轻服务器压力。当然要实现缓存,要设置缓存文件,在初始化okHttpClient时候设置缓存文件

//设置缓存文件路径,和文件大小
okHttpClent.cache(new Cache(new File(Environment.getExternalStorageDirectory() +          "/okhttp_cache/"), 50 * 1024 * 1024))
                

查阅大量的资料,大量博客都很坑。坑的你懵逼这里给大家看下正解:okhttp缓存正解

 看到这篇的时候,你就知道其他地方都可以不用管,用拦截器可以实现,但是要清楚什么是在线缓存,什么是离线缓存。这是2个概念。在okHttpClient加上网络拦截器如下:

okHttpClient.addNetworkInterceptor(NetCacheInterceptor.getInstance())

这里用的拦截器,我使用了单例,这样便于通过改变参数,可以达到是否使用缓存;NetCacheIntertor代码如下:

/**
 * Created by leo
 * on 2019/7/25.
 * 在有网络的情况下
 * 如果还在网络有效期呢则取缓存,否则请求网络
 * 重点 : 一般okhttp只缓存不大改变的数据适合get。(个人理解 : 例如你设置了我的方案列表接口的缓存后,你删除了一条方案,刷新下。
 * 他取的是缓存,结果那条删除的数据会出来。这个时候这个接口,不适合用缓存了)
 * (这里注意,如果一个接口设置了缓存30秒,下次请求这个接口的30秒内都会去取缓存,即使你设置0也不起效。因为缓存文件里的标识里已经有30秒的有效期)
 */
public class NetCacheInterceptor implements Interceptor {
    private static NetCacheInterceptor cacheInterceptor;
    //30在线的时候的缓存过期时间,如果想要不缓存,直接时间设置为0
    private int onlineCacheTime;

    public static NetCacheInterceptor getInstance() {
        if (cacheInterceptor == null) {
            cacheInterceptor = new NetCacheInterceptor();
        }
        return cacheInterceptor;
    }

    private NetCacheInterceptor() {

    }

    public void setOnlineTime(int time) {
        this.onlineCacheTime = time;
    }


    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request.Builder builder1 = request.newBuilder();
        //这里我们登陆,在head里获取令牌token存起来,网络请求的时候把令牌加入head,用于身份区分
        String token = (String) PreferenceUtil.get("USER_TOKEN", "");
        if (!TextUtils.isEmpty(token)) {
            builder1.addHeader("Token", token)
                    .build();
        }
        request = builder1.build();
        Response response = chain.proceed(request);
        List<String> list = response.headers().values("Token");
        if (list.size() > 0) {
            PreferenceUtil.put("USER_TOKEN", list.get(0));
        }

        

        //这里是设置缓存的操作
        if (onlineCacheTime != 0) {
            //如果有时间就设置缓存
            int temp = onlineCacheTime;
            Response response1 = response.newBuilder()
                    .header("Cache-Control", "public, max-age=" + temp)
                    .removeHeader("Pragma")
                    .build();
            onlineCacheTime = 0;
            return response1;
        } else {
            //如果没有时间就不缓存
            Response response1 = response.newBuilder()
                    .header("Cache-Control", "no-cache")
                    .removeHeader("Pragma")
                    .build();
            return response1;
        }
//        return response;
    }
}

max-age是在线缓存的有效时间,如果我设置了max-age = 3600,那么意思是在首次请求网络缓存下来的数据后,在1小时之内都将会直接取缓存,跳过网络请求。这里我获取token是通过拦截器获取的,response.headers()可以获得,服务器返回的所有head信息,包括set-cookie信息。而且okhttp提供了.cookjar()。可以通过cookie持久化等自定义

这些设置完怎么验证呢?回到你原始网络请求onResponse回调里;通过:

                if (response.networkResponse()!=null){
                    LogUtils.i("内容来源","来自网络请求");
                }
                if (response.cacheResponse()!=null){
                    LogUtils.i("内容来源","来自缓存");
                }

当然这里只是验证,在onResponse不需要改变,okhttp内部已经做好了所有的工作。

6、无网络的时候的离线缓存

例如腾讯新闻等,在你手机开启飞行模式的时候,在进app的时候,还是会依旧显示之前加载的数据。这个是就是离线缓存,离线缓存和在线缓存最大的区别,在线缓存即使有条件请求网络也可以跳过网络取缓存。同样通过拦截器添加,在无网络的时候,是不会走addNetworkInterceptor方法的。但是通过addInterceptor,有没有网都会走,而且addInterceptor会先于addNetworkInterceptor运行

okHttpClient.addInterceptor(OfflineCacheInterceptor.getInstance())

同样我也用了单例,具体如下:

/**
 * Created by leo
 * on 2019/7/25.
 * 这个会比网络拦截器先 运行
 * 在没有网络连接的时候,会取的缓存
 * 重点 : 一般okhttp只缓存不大改变的数据适合get。(个人理解,无网络的时候可以将无网络有效期改长点)
 * 这里和前面的不同,立即设置,立即生效。例,你一个接口设置1个小时的离线缓存有效期,立即设置0.下次进入后,则无效
 */
public class OfflineCacheInterceptor implements Interceptor {
    private static OfflineCacheInterceptor offlineCacheInterceptor;
    //离线的时候的缓存的过期时间
    private int offlineCacheTime;

    private OfflineCacheInterceptor() {

    }

    public static OfflineCacheInterceptor getInstance() {
        if (offlineCacheInterceptor == null) {
            offlineCacheInterceptor = new OfflineCacheInterceptor();
        }
        return offlineCacheInterceptor;
    }

    public void setOfflineCacheTime(int time) {
        this.offlineCacheTime = time;
    }

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

        if (!NetWorkUtils.isNetworkConnected(MyApplication.getContext())) {
            if (offlineCacheTime != 0) {
                int temp = offlineCacheTime;
                request = request.newBuilder()
//                        .cacheControl(new CacheControl
//                                .Builder()
//                                .maxStale(60,TimeUnit.SECONDS)
//                                .onlyIfCached()
//                                .build()
//                        ) 两种方式结果是一样的,写法不同
                        .header("Cache-Control", "public, only-if-cached, max-stale=" + temp)
                        .build();
                offlineCacheTime = 0;
            } else {
                request = request.newBuilder()
                        .header("Cache-Control", "no-cache")
                        .build();
            }
        }
        return chain.proceed(request);
    }
}

NetWorkUtils是一个判断有没有网络的工具类。你可以看到这里是max-stale,这里我理解就是要设置的离线缓存有效期,如果设置为max-stale=3600就是离线缓存1个小时。如果你去深山老林,在长达1小时01分的时候,离线缓存失效,那么你在此进app,页面将空白。当然你也可以设置离线缓存一直有效,Integer.MAX_VALUE。

7、多次请求同一url,在网络请求未结束,是否只请求一次。

这里当然你可以利用tag来做,如果当前正在请求的池里的call.request.tag()或等待请求队列里的tag,包含你当前请求网络tag时,则不请求网络,只显示loading。但是这样的话必须每次都要加上tag。所以我直接在EasyOk里加上了一个

//防止网络重复请求的tagList;
private ArrayList<String> onesTag;

如果没有tag的时候,这里我可以用请求url。在网络请求成功和结束的时候我从这个集合里remove掉这个元素。当然你会说,当我们取消网络请求的时候呢,其实取消网络请求的时候会走onFailure(Call call,IOException e)。这个时候错误类型是SocketException。所以你取消网络的时候是会走onFailure,同样会remove掉、代码大致如下(只放相关代码):

public void enqueue(final ResultCall resultMyCall) {
        if (resultMyCall != null) {
            //这里是子线程切换到主线程的操作,只要请求网络,我们都调onBefore,这里就是展示loading
            mDelivery.post(new Runnable() {
                @Override
                public void run() {
                    resultMyCall.onBefore();
                }
            });
        }

        
        //这里是否带了onlyOneNet参数,默认是不开启的,return后将不继续往下走,就不会开启网络了
        
        if (onlyOneNet) {
            if (!TextUtils.isEmpty(tag)) {
                if (EasyOk.getInstance().getOnesTag().contains(tag)) {
                    return;
                }
                EasyOk.getInstance().getOnesTag().add(tag);
            } else {
                if (EasyOk.getInstance().getOnesTag().contains(url)) {
                    return;
                }
                EasyOk.getInstance().getOnesTag().add(url);
            }
        }

        ...

}

 

8、请求失败自动重连,以及重连次数

这里也查阅了大量博客,都说okhttp有设置重连,设置okHttpClient.retryOnConnectionFailure(true)既可用重连,但是我大量测试发现,然并软(有明白的小伙伴,求告知)。这里我用了自己的方式,只要走onFailure,那么我们看看有没有设置重连和重连次数,代码如下,此代码都在builder下,每次网络请求都会new一个builder,以OkGetBuilder为例:

okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                //这里是取消网络请求,那么不用重连了
                if (e instanceof SocketException) {

                } else {
                    //tryAgainCount是重连,不设置,默认是0.不开启重连功能
                    //currentAgainCount,是OkGetBuilder里的属性,每次开启网络都会new一个Builder
                    //当然currentAgainCount初始是0
                    if (currentAgainCount < tryAgainCount && tryAgainCount > 0) { 
                        currentAgainCount++;
                        //这里就是重连操作,call.request会获取你最开始的request
                        //this这里就是你当前new Callback,所以网络回调还会走这里
                        okHttpClient.newCall(call.request()).enqueue(this);
                        return;
                    }
                }
        
        }

 

 

至此,大致介绍完了。以上介绍的到底怎么用可以去我的github看看以上的用法

9、那么基于mvc封装怎么用,下面我们具体来说说这块

我定义了NetWorListener(get,post,上传文件网络回调),OnDownloadListener(文件下载网络回调),PermissionListener(权限申请结果回调),如果你想请求网络,只要实现这个接口,然后通过ModelSuperImpl去调用网络请求,就能在任何你实现这个接口的页面拿到网络请求回调,请求只需要这样:

调用网络:
//在外面调用只需要传入参数,把url和解析类什么的都放在ModelSuperImpl里
ModelSuperImpl.netWork().gankGet(ParamsBuilder.build().params(PARAMS.gank("android")) .command(GANK_COMMAND), this);

 

ModelSuperImpl大致如下:

public class ModelSuperImpl extends ModelBase {
    private static final ModelSuperImpl ourInstance = new ModelSuperImpl();

    public static ModelSuperImpl netWork() {
        return ourInstance;
    }

    public static ModelPermissionImpl permission() {
        return new ModelPermissionImpl();
    }

    private ModelSuperImpl() {

    }

   
    public void gankGet(ParamsBuilder paramsBuilder, NetWorkListener netWorkListener) {
        paramsBuilder.url(SystemConst.GANK_GET)
                .type(new TypeToken<ResponModel<User>>() {
                }.getType())
        ;
        sendOkHttpGet(paramsBuilder, netWorkListener);
    }

}

 

在Activity/Fragment里或是只要实现接口的地方拿到回调就是这样,command是为了区分一个页面可能请求多个网络请求:

@Override
public void onNetCallBack(int command, Object object) {
    switch (command) {
        case GANK_COMMAND:
            Response<User> userModel = (Response<User>)object;
            break;
    }
}

 

这里的ParamsBuilder有多个参数具体如下:

/**
 * Created by leo
 * on 2019/7/11.
 */
public class ParamsBuilder {
    //请求网络的url(必填)
    private String url;
    //网络回调的int值(必填)
    private int command;
    //网络返回的type类型(选填)不填,则会返回string类型
    private Type type;
    //网络请求需要带的头部信息(选填,不填为null)
    private HashMap<String, String> heads;
    //网络请求需要带的参数(选填,不填为null)
    private HashMap<String, String> params;
    //网络loading需要带的文字信息(选填,不填为null)
    private String loadMessage;
    //是否显示网络loading(默认为显示loading)
    private boolean isShowDialog = true;
    //网络请求的tag,可根据tag取消网络请求(选填,不填:默认当前宿主类名,退出后自动取消)
    private String tag;
    //是否重写网络问题还是超时问题对回调进行一个重写
    //如果是true,则在回调的时候可对那部分额外操作,除了弹提示还可以做别的操作
    //(选填,不填:重写不了且只弹提示)
    private boolean overrideError;
    //json上传要带的参数
    private String json;
    //网络接口code=200, 但没有成功,此用户已关注
    //需要重写带true,重写可以写逻辑包括弹提示
    //不需要重写只弹提示
    private boolean successErrorOverrid;

    //离线缓存时间  单位秒
    private int cacheOfflineTime;
    //有网络请求时缓存最大时间
    private int cacheOnlineTime;
    //多次点击按钮,只进行一次联网请求
    //场景:网络还在loading,又点了一次请求,那么不发送新请求,只显示loading
    private boolean onlyOneNet = true;
    //联网失败,重试次数
    private int tryAgainCount;
    //如果是在网络请求接口回调不是activity,也不是fragment,用于传context
    
    //用于showdialog,当请求网络的页面不是Activity或是Fragment时必传
    private Context context;


    /**
     * 下载文件才用的到
     */
    private String path;
    private String fileName;
    //是否开启断点续传,要注意的是开启断点续传,要保证下载的是同一文件
    //默认是不开启断点续传,除非判断要下载文件和当前未下载文件属于同一文件
    //如果不是那么重新下载,会清掉之前的文件。
    private boolean resume;

}

看到上面,具体设计的参数都写上了。但是我没有封装的很完美,当然必传的字段,没传时,你可以throw new NullPonintException("参数没传");把异常抛出去,一旦不传,程序就崩溃了。同样在下载文件只需要实现OnDownliadListener,即可。调用只需要这样:

ModelSuperImpl.netWork().downApk(ParamsBuilder.build().path(path)
        .fileName(fileName).tag("downApk"), this);

这里我把请求权限所有逻辑封装在了 ModelPerissionImpl里,只要实现PerimissionListener即可拿到网络请求回调,调用只需这样:

//RESUME_COMMAND一个页面可能要请求多个权限,用于区分,this即是PerimissionListener实现类,后面权限参数是可变的如果有多个权限可以一直逗号加下去
ModelSuperImpl.permission().requestPermission(RESUME_COMMAND, this, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE);


//回调只需要这样;command是区分一个页面多个权限申请,如果一个页面只有1个申请那么可以不传commad
@Override
    public void permissionSuccess(int command) {
        switch (command) {
            case NORMAL_COMMAND:
                ModelSuperImpl.netWork().downApk(ParamsBuilder.build().path(path)
                        .fileName(fileName).tag("downApk"), this);
                break;
           
        }
    }

 

大致思路:我把具体的联网操作和具体的解析封装在了抽象类ModelBase,把具体的权限申请逻辑封装在了ModelPerissionImpl,ModelSuperImpl只负责调用网络请求方法,和权限申请方法,这样代码完美分隔了代码。视图层View收到了用户的操作或点击请求,响应controller,controller通知model处理逻辑业务,拿到结果通过接口告诉controller去更新Ui。加入你多个页面需要请求同一个url,你只需要通过ModelSuperImpl去调用方法就Ok了。

 

这里肯定很多人对ParamBuilder里的overrideError和successErrorOverrid不是很理解。其实这里默认不是重写方法,例如网络请求失败,我在ModelBase默认是弹出toast,如果是code=200,但是有可能接口走的错误方法,如关注失败,我也是默认不重写弹出toast。有可能实际操作需要我们做别的,如网络请求错误,需要我们跳另外一个页面,那么这个时候就需要你带.overrideError(true)和.successErrorOverrid(true)代码如下:

 

@Override
    public void onNetCallBack(int command, Object object) {
        switch (command) {
            case GANK_COMMAND:
                /**
                 * 请求接口失败(网络错误,或超时等造成)
                 * 如果需要重写则请求接口的时候加上.overrideError(true)
                 * 那么在下面代码写上逻辑,如果不需要重写,已经封住了会自动弹出错误提示,而且重            
                    写会无效
                 * */

//                if (obj instanceof NetFail) {
//                    NetFail netFailBean = (NetFail) obj;
//                    处理逻辑
//                    return;
//                }

                /**
                 * 这是接口请求成功
                 * 如果请求接口,写了.successErrorOverrid(true)
                 * 说明重写了虽然code=200,返回的result 不是1;但是需要做其他逻辑不只是Toast才需要true
                 * 如果只是需要Toast错误信息,那么可以不写,下面的就不用重写了。封装已经默认弹提示
                 * */
//                if (obj instanceof ErrorBean) {
//                    ErrorBean errorBean = (ErrorBean) obj;
//                    处理逻辑
//                    return;
//                }


                
                ResponModel<User> detailModel = (ResponModel<User>) obj;
                //更新UI
                break;
        }
    }

  结束语:我个人思路封装。如有不对欢迎指正,且如果有更好的思路欢迎留言。技术界的小学生,喜欢学习。看到这里,如果有帮助到你,帮博主star下吧

github地址

记得把builder里的EventBus全部删除,我之前没考虑太多,所以在展示效果的时候用EventBus传递了p.p

当然这里用的泛型类ResponModel,ErrorBean,NetFail都是我根据我的项目来定义的,记得如有数据结构不同请修改成需要的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值