本身封裝一個okhttp,一個看的懂的okhttp封裝

前言:封裝只是加深本身的理解,網上已經有很優秀的封裝,我也是借鑑了okgo鴻洋的okhttputils。本項目是基於mvc模式下,但這篇只講如何對okhttp進行封裝(這裏我按最基礎步驟來,須要額外功能,看源碼和本文理解,確定能夠實現)java

咱們封裝要有的功能有:android

  • 支持get請求
  • 支持post請求
  • 支持上傳文件
  • 支持下載文件和斷點續傳
  • 有網絡時,支持緩存(鏈接網絡時的有效期)
  • 斷開網絡,支持離線緩存(離線緩存有效期)
  • 屢次請求同一url,在網絡還在請求時,是否只請求一次
  • 支持請求失敗,自動重連

先看看我封裝的效果(建議打開權限)

get請求 post請求 上傳文件
下載文件

首先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,要把他切換到主線程裏。git

一、封裝EasyOk

1.1 取消網絡請求

//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這裏就要用到單例了github

1.2 簡單的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,這裏借鑑了鴻洋大神的思路。由於咱們最好把重複性的工做所有都放在封裝裏。封裝最重要的優勢不就是爲了方便嗎。json

根據最原始的okhttp進行的get請求,咱們還缺Request,還有一個網絡請求的回調,那麼請看下面。api

二、封裝OkGetBuilder(這裏我將每種請求封裝成了不一樣的Builder,雖然重複了好多操做,但更加清晰,偏於理解)

由於每次請求,Request 請求體都是須要new的,因此可想而知這裏不多是單例,並且每次調用請求都是new出來的Request。根據最原始的get請求,咱們知道OkGetBuilder裏須要一、url,二、參數,三、header,四、tag,五、還有本身的網絡回調。緩存

2.1 自定義請求回調抽象類(基於mvc二次封裝建議用接口類)

因此咱們得用有個網絡回調接口,這裏我用的是抽象類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,是須要打開另一個頁面則能夠重寫這個方法如:cookie

@Override
            public void onError(String errorMessage) {
                super.onError(errorMessage);
                //註釋super.onError(errorMessage);那麼不會走父類方法,
 
            }
複製代碼

2.2 簡單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 {
            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);
                            }
                        });
                    }
                }

            }
        });
    }
    
    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方法。網絡

那麼接下來就是請求這塊

  • 在調用自定義方法enqueue(我這裏和okhttp重名了偏於理解),首先咱們這裏就要回調onBefore
  • 在原始回調失敗onFailure裏,固然是回調咱們的onAfter和onError。
  • 在原始回調onResponse裏,這裏會比較麻煩,即便返回code=200;這裏還有2種狀況,一種是正常的按你傳入的泛型解析,一種是好比:點擊關注,網絡請求也成功了,但接口問題返回關注失敗。咱們公司用的是status爲0表明失敗,這裏要特別注意
  • OkGetBuilder封裝好了,把它放進EasyOk裏以下:
public static OkGetBuilder get() {
        return new OkGetBuilder();
    }
複製代碼

三、最後封裝後的進行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;
 
                }
            });
複製代碼

上面介紹我我把緩存和重連等其餘功能沒有說,由於加進去太複雜也不清晰,等後面單獨拿出來說,經過本文的理解,你會知道怎麼去封裝,加到什麼地方去。

四、接下說的是下載文件及斷點續傳下載文件

通過上述介紹,我們直接看OkDownloadBuilder裏的內容(我把多餘部分去掉了,便於理解):

public class OkDownloadBuilder {
    //斷點續傳的長度
    private long currentLength;
    private String url;
    private String tag;
    //文件路徑(不包括文件名)
    private String path;
    //文件名
    private String fileName;

    //是否開啓斷點續傳
    private boolean resume;
    private OkHttpClient okHttpClient;
    private Handler mDelivery;
    private Request.Builder mBuilder;

    public OkDownloadBuilder() {
        this.okHttpClient = EasyOk.getInstance().getOkHttpClient();
        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 enqueue(final OnDownloadListener listener) {

        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) {
                //下載失敗監聽回調
                mDelivery.post(new Runnable() {
                    @Override
                    public void run() {
                        listener.onDownloadFailed(e);
                    }
                });
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                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) {

                    }

                }


            }
        });

    }
}
複製代碼

若是你不看斷點續傳這塊,在onResponse裏其實就是輸入流和文件流,把流寫進文件裏的操做;斷點的續傳的關鍵點是哪些?要知道斷點續傳其實就是接着上次未下載的文件繼續下載(固然這裏要確保下載的是同一文件,以下載更新要保證是同一版本號,這裏在得到版本更新內容的時候判斷是不是同一版本)。關鍵有2點:

  • 在開啓文件下載的時候,要在header里加上當前文件長度
mBuilder.header("RANGE", "bytes=" + currentLength + "-");
複製代碼
  • 同時在文件流的時候要告訴流,咱們不是覆蓋,是拼接
fos = new FileOutputStream(file, true);//沒錯就是這個true
複製代碼

五、有網的時候的在線緩存

場景以下:假如首頁廣告,get請求下來的數據,並且這個可能1個星期纔會換一次數據,這個時候若是沒有這個功能,每次進首頁都會去請求網絡,若是有這功能,那麼若是緩存內容在有效期就會跳過網絡請求,直接取緩存。這樣節約流量之餘還能減輕服務器壓力。固然要實現緩存,要設置緩存文件,在初始化okHttpClient時候設置緩存文件

//設置緩存文件路徑,和文件大小
okHttpClent.cache(new Cache(new File(Environment.getExternalStorageDirectory() +"/okhttp_cache/"), 50 * 1024 * 1024))
複製代碼

查閱大量的資料,大量博客都很坑。坑的你懵逼這裏給你們看下正解:okhttp緩存正解,文字地址
看到這篇的時候,你就知道其餘地方均可以不用管,用攔截器能夠實現,可是要清楚什麼是在線緩存,什麼是離線緩存。這是2個概念。在線緩存須要加網絡攔截器

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內部已經作好了全部的工做。

六、無網絡的時候的離線緩存

例如騰訊新聞等,在你手機開啓飛行模式的時候,在進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。

七、屢次請求同一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);
            }
        }
 
        ...
 
}
複製代碼

八、請求失敗自動重連,以及重連次數

這裏也查閱了大量博客,都說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上的介紹,看看以上的用法

九、那麼基於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裏或是隻要實現接口(NetWorListener)的地方拿到回調就是這樣,command是爲了區分一個頁面可能請求多個網絡請求:

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

這裏的ParamsBuilder有多個參數具體以下(去掉部分代碼,偏於理解):

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都是我根據個人項目來定義的,記得若有數據結構不一樣請修改爲須要的

相關文章
相關標籤/搜索