對Retrofit進(jìn)行簡單封裝,提供良好適配節(jié)點(diǎn)

在寫這個框架的時候,技術(shù)Leader跟我強(qiáng)調(diào)過一句話:’封裝是為了更方便使用,但不要為了封裝而封裝‘
也正是因為這句話,我在寫完beta版本時,又重新審視了一遍,果然發(fā)現(xiàn)不少可以精進(jìn)的地方,最終才有了現(xiàn)在這樣清晰結(jié)構(gòu),感受頗深。

本篇是在幾個項目都使用Rxjava 、Retrofit+OkHttp后總結(jié)的知識與經(jīng)驗的基礎(chǔ)上,綜合考慮過項目開發(fā)需求、開發(fā)使用習(xí)慣后而產(chǎn)生的封裝思路。

先放一張框架結(jié)構(gòu)圖

tbretrofitV2.0.png

這是一次常規(guī)請求從發(fā)起到結(jié)束的整個過程,綠框中的內(nèi)容就是需要封裝來做的事情。
第一次畫這個,很蹩腳,東西不多還算清晰吧 哈哈

如果你更關(guān)心OkHttp 如何使用Interceptor 來解決 session ,token 過期后 同步重新獲取再繼續(xù)原本的請求,可以參照我的實(shí)例 SignInvalidInterceptor.java

封裝后解決的核心問題:

1.簡化接口的冗余代碼及線程調(diào)度代碼
2.合理的釋放線程和Context解綁釋放
3.為加解密提供合理易拆分的入口
4.提供可選擇的多種緩存模式
5.更清晰的攔截網(wǎng)絡(luò)異常,優(yōu)雅的下發(fā)異常信息

使用示例
GET:

      HttpUtils.getHttpApi() .get(GITHUB_RESTFUL, new HttpCallBack<GithubEntity>(RxHttpTest.this) {
                    @Override
                    public void onSuccess (GithubEntity githubEntity) {
                        printLog("githubEntity:" + githubEntity.getName());

                    }
                    @Override
                    public void onFailure (int errorCode, String message) {
                        printLog("onFailure  errorCod:" + errorCode + "  errorMsg:" + message);
                    }
                    @Override
                    public CacheModel cacheModel () {
                        return CacheModel.NORMAL;
                    }

                });
    }

POST使用:

   HttpUtils.getHttpApi() .postJson(API.loginUrl, PostDataUtils.getSiginParameter(), new HttpCallBack<SiginResponseBean>(RxHttpTest.this) {
                    @Override
                    public void onSuccess (SiginResponseBean s) {
                    }
                    @Override
                    public CacheModel cacheModel () {
                        return CacheModel.NORMAL;
                    }
                });

看起來好像其實(shí)沒什么區(qū)別,幾乎和原來的Retrofit 接口差不多,并不影響使用習(xí)慣和代碼風(fēng)格

封裝一個庫不僅僅是為了減少重復(fù)代碼,也應(yīng)該解決一些使用中的問題
下面記錄此次加入Rxjava到其中后產(chǎn)生的新思路

一、為什么這樣封裝以及如何用封裝解決

1.用過Retrofit +Rxjava 的都知道,需要為不同接口寫不同的Service,使用GsonConvertfactory可以在每個接口返回值直接返回ResultObject。 但這樣面臨的問題是什么呢?

1).我們需要為每一個接口都在Service 層多加一個接口,想我之前直接使用Service。項目比較大單個Service中至少寫了60多個方法,它僅僅是一個接口,加上必要注釋大概有300多行。這其中包含大量的重復(fù)代碼,比如一樣的返回值,一樣的ip引用,僅僅是對應(yīng)后臺的接口名不同,這顯然需要處理掉
解決思路:Retrofit 提供的接口非常豐富,但常用的無非就是Get 、Post、Formdata、和支持文件的Multipart類型。
下面是準(zhǔn)備的常用接口:

為什么是Response<String>而不是Object下文會有解釋

    @GET
    Observable<Response<String>> get(@Header("Cache-Control") String cacheControl, @Url String url);
    @GET
    Observable<Response<String>> get(@Header("Cache-Control") String cacheControl,@Url String url, @QueryMap Map<String, Object> map);
    @POST
    @FormUrlEncoded
    Observable<Response<String>> postForm(@Header("Cache-Control") String cacheControl,@Url String url, @FieldMap Map<String, Object> map);
    @POST
    @Headers("Content-Type:application/json;")
    Observable<Response<String>> postJson(@Header("Cache-Control") String cacheControl,@Url String url, @Body Object body);
    @POST
    Observable<Response<String>> postIndependent(@Header("Cache-Control") String cacheControl,@Url String url, @Body RequestBody body);
    @POST
    Observable<Response<String>> postFormDataFiles(@Header("Cache-Control") String cacheControl,@Url String url, @Body MultipartBody body);

這里為了方便閱讀,去掉了注釋,如果有對Retrofit支持的接口類型不熟悉的可以自信查閱后再閱讀本篇。
為了保證良好的拓展,專門準(zhǔn)備了支持RequestBody作為最高級支持的參數(shù)類型

2).使用RxJava 是為了方便管理線程和數(shù)據(jù)轉(zhuǎn)換,為此需要為每一個接口都寫一遍線程調(diào)度,處理異常和管理內(nèi)存釋放。但其實(shí)通常在Android 中接口請求都是異步的,并不是每一個接口都需要去更換線程調(diào)用方式和數(shù)據(jù)轉(zhuǎn)換。
解決思路:既然線程調(diào)度可以保持不變,那就用一個模式搞定他,有了問題1中的接口,就需要一個類似于港口的類來負(fù)責(zé)轉(zhuǎn)接(可以理解為代理,但他并不是代理模式)。
源碼實(shí)例:

public interface HttpApi {
    void get(String url,HttpResponseListener responseListener);
    void get(String url, String[] values,HttpResponseListener responseListener);
    void get(String url, Map<String, Object> map,HttpResponseListener responseListener);
    void postJson(String url, JsonBody json,HttpResponseListener responseListener);
    void postRequestBody(String url, RequestBody body,HttpResponseListener responseListener);
    void postFormData(String url, Map<String, Object> map ,HttpResponseListener responseListener);
    void postFormDataFiles(String url, Map<String, Object> map, List<File> files, MediaType contentType,HttpResponseListener responseListener);
}


final public class RxHttpApiImpl implements HttpApi {
 代碼省略······
     private  void subscribe (final Observable<Response<String>> observable, final HttpResponseListener responseListener) {
        if (null == observable) return;
        final Subscriber<Response<String>> subscriber = new ResponseHandler(responseListener);
        observable.subscribeOn(Schedulers.io())//被觀察者創(chuàng)建線程 事件產(chǎn)生的線程 變量X
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())//觀察者接受回調(diào)線程 事件接受線程 應(yīng)變量Y
                .retryWhen(new RetryWhenTimeout())
                .doOnSubscribe(new Action0() {
                    @Override
                    public void call () {
                        httpTaskManagement.addSubscription(responseListener.getContext(), subscriber);
                    }
                })
                .doOnUnsubscribe(new Action0() {
                    @Override
                    public void call () {
                        httpTaskManagement.remove(responseListener.getContext());
                    }
                })
                .subscribe(subscriber);
    }

  @Override
    public void get (String url, HttpResponseListener responseListener) {
        subscribe(apiService.get(checkCacheModel(responseListener.cacheModel()),url), responseListener);
    }

 代碼省略······
}

HttpApi 是面向外部的接口,而在實(shí)現(xiàn)類RxHttpApiImpl則負(fù)責(zé)實(shí)現(xiàn)統(tǒng)一的線程調(diào)度和解綁
這里解綁包含Subscriber解綁和Context與網(wǎng)絡(luò)框架解綁。
與Context解綁就是上面doOnUnsubscr中用到的HttpTaskmanagement處理的。
代碼如下:

    代碼省略······
    @Override
    public void unSubscribe(Object tag) {
        if (null == tasks || tasks.isEmpty()) return;
        RxHttpLog.printI("RxHttpTaskmanagement", "執(zhí)行取消訂閱 key:" + tag.hashCode());
        Subscription subscription = tasks.get(tag);
        if (null != subscription && !subscription.isUnsubscribed()) {
            subscription.unsubscribe();
            RxHttpLog.printI("RxHttpTaskmanagement", "取消了訂閱 key:" + tag.hashCode());
            tasks.remove(tag);
        }
    }

    代碼省略······

Subscriber解綁:

   @Override
    public void onStart () {
        super.onStart();
        responseListener.onStart();
        //攔截?zé)o網(wǎng)絡(luò)可用
        if (!NetworkStatusUtils.networkIsConnected(responseListener.getContext())) {
            responseListener.onFailure(HttpCode.CODE_NO_INTERNET, "網(wǎng)絡(luò)不可用");
            unsubscribe();
            onCompleted();
        }
    }

3).因為使用了GsonConvertFactory,所以入?yún)⒑头祷囟际荍SON格式的。而且必須是Object子類去做,那如果要加密怎么辦?顯然這樣是無法滿足的,只想讓他作為網(wǎng)絡(luò)請求的橋梁而非限制如何使用
這里有兩種思路去插入加密解密的地方:
a.一個是直接為OkHttp添加Intercepter
這種方法比較死板,容易出錯。因為攔截器我們攔截到的是Retrofit中編碼后的數(shù)據(jù),這樣并不利于我們直接操作。通常一個項目中,我們使用的client為了保持一致都是單例的,添加了加密的攔截器有加密的無加密的就無法拆分。所以我并沒有實(shí)踐這種思路
b.重寫Convertfactory
在傳給OkHttp 之前就進(jìn)行加密操作,在OkHttp正常返回后進(jìn)行解密。
加解密我并沒有世界操作,但我大概肯定下面的方法是可行的

加密處
GsonRequestBodyConverter源碼 :

   @Override 
    public RequestBody convert(T value) throws IOException {
        Buffer buffer = new Buffer();
        Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
        JsonWriter jsonWriter = gson.newJsonWriter(writer);
        adapter.write(jsonWriter, value);
        jsonWriter.close();
        return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
    }

可以看到這里重新進(jìn)行了編碼,那我們只用對buffer進(jìn)行加密即可

解密處
StringResponseBodyConverter:

 @Override
    public String convert (ResponseBody value) throws IOException {
        //這里不用攔截 contentLength ==-1?: 源碼中轉(zhuǎn)string 時已經(jīng)判斷了
        RxHttpLog.printI("StringResponseBodyConverter","ResponseBody:"+value.contentLength());
        try {
            return value.string();
        } finally {
            value.close();
        }
    }

4.)關(guān)于緩存的實(shí)現(xiàn)思路
a.為Okhttpclient 添加Intercepter
死板 不利于實(shí)現(xiàn)有緩 無緩兩種方案
b.利用Retroift支持@Header注解在接口出作為參數(shù)添加Cache-Control
實(shí)現(xiàn)代碼:

  @GET
    Observable<Response<String>> get(@Header("Cache-Control") String cacheControl, @Url String url);
public class CacheConfig {

    /**
     * CacheControl.Builder :
     - noCache();//不使用緩存,用網(wǎng)絡(luò)請求
     - noStore();//不使用緩存,也不存儲緩存
     - onlyIfCached();//只使用緩存
     - noTransform();//禁止轉(zhuǎn)碼
     - maxAge(10, TimeUnit.MILLISECONDS);//超過 maxAge 將走網(wǎng)絡(luò)。
     - maxStale(10, TimeUnit.SECONDS);//超過 maxStale 緩存將不可用
       但依然由 maxAge決定是否走網(wǎng)絡(luò),所以 精良讓maxAge<maxStale 避免返回 空結(jié)果

     - minFresh(10, TimeUnit.SECONDS);//超時時間為當(dāng)前時間加上10秒鐘。

     */


    /**
     * 不考慮緩存,直接走網(wǎng)絡(luò),但會存儲響應(yīng)到緩存區(qū)
     * @return
     */
    public static String forceNetWork () {

        return CacheControl.FORCE_NETWORK.toString();
    }

    /**
     * 不使用緩存,并且不會存儲響應(yīng)到緩存區(qū)
     * @return
     */
    public static String forceNetWorkAndNoStore () {
        return new CacheControl.Builder()
                .noCache()
                .noStore()
                .build().toString();
    }

    /**
     * 直接讀取緩存區(qū)
     * 如果已有緩存,則將緩存可用時間設(shè)置為 Integer.MAX_VALUE
     * 如果緩存區(qū)無數(shù)據(jù)則返回null  code: 504-unsatisfiable request
     * @return
     */
    public static String forceCache () {

        return CacheControl.FORCE_CACHE.toString();
    }


    /**
     * 完全交給Cache-Control:value
     * 由value來決定 重新請求還是使用緩存
     * 另外:Cache-Control 區(qū)分 client  與 server,
     * 也就是服務(wù)器對應(yīng)的 expires 有效時間   client max-age
     * 并且由 client 為準(zhǔn) server 為輔
     *
     * max-age client自主決定緩存有效期 超過有效期才請求網(wǎng)絡(luò)
     * 但是如果服務(wù)器依然返回304 則將繼續(xù)使用緩存
     *
     * @return
     */
    public static String normal () {
        return new CacheControl.Builder()
                .maxAge(30, TimeUnit.SECONDS)
                .build().toString();
    }

    /**
     * 針對近似永久緩存數(shù)據(jù),使用如下策略
     * 接口配置 只使用緩存,加入無緩存 504 攔截器
     * 為此次任務(wù)重新配置cache-control 在首次讀取時從網(wǎng)絡(luò)下載一次
     * @return
     */
    public static String forever () {
        return new CacheControl.Builder()
                .maxAge(1, TimeUnit.SECONDS)
                .maxStale(Integer.MAX_VALUE,TimeUnit.SECONDS)
                .build().toString();
    }
}

5).關(guān)于OkhttpClient網(wǎng)絡(luò)異常(不包含40x 50x 這類正常狀態(tài)碼,這些交給調(diào)用層按需求處理),通常我們只判斷了手機(jī)的網(wǎng)絡(luò)開關(guān)與模式,不易與發(fā)現(xiàn)其他網(wǎng)絡(luò)異常。Retroift +RxJava輕松幫我們都攔截到,我們只需單獨(dú)判斷出來即可。
經(jīng)過細(xì)致測試,按照由先到后,又子類到父類的策略攔截異常,保證能抓取到準(zhǔn)確的異常信息
代碼如下:

 /**
     * 協(xié)議層 讀存數(shù)據(jù)發(fā)生錯誤才會走這里
     *
     * @param e
     */
    @Override
    public void onError (Throwable e) {
        //此處可攔截到的異常均發(fā)生在請求發(fā)起以后
        RxHttpLog.printI(TAG, "onError Throwable subClass :" + e.getClass() + " errorMsg:"+e.getMessage());
        if(e instanceof NullPointerException){
              /*
               * 響應(yīng)返回或者緩存返回為null
               *此處的空指針來自于StringConverterFactory中轉(zhuǎn)換響應(yīng)結(jié)果時發(fā)生,會先調(diào)用onResponse 才會掉onError
               * 所以這里不下發(fā)給onFailure
               */
        }else if(e instanceof UnknownHostException){
            responseListener.onFailure(HttpCode.CODE_UNKNOW_HOST, "訪問的目標(biāo)主機(jī)不存在");

        }else if (e instanceof HttpException) {
            //SSL 驗證成功 有正常的 響應(yīng)返回,服務(wù)器無包裝時 協(xié)議碼為標(biāo)準(zhǔn) 401 404 等。。。
            HttpException httpException = (HttpException) e;
            responseListener.onFailure(httpException.code(), httpException.message());

        } else if (e instanceof SSLException) {
            //無法鏈接目標(biāo)主機(jī)-本地網(wǎng)絡(luò)無法訪問外部服務(wù)器地址-服務(wù)器地址無法連接
            responseListener.onFailure(HttpCode.CODE_INTENET_IS_ABNORMAL, "無法與目標(biāo)主機(jī)建立鏈接");

        } else if (e instanceof ConnectException
                || e instanceof SocketTimeoutException
                || e instanceof TimeoutException) {
            responseListener.onFailure(HttpCode.CODE_TIME_OUT, "鏈接超時");

        } else  if (e instanceof IOException) {
            //有網(wǎng)但請求失敗包含:網(wǎng)絡(luò)未授權(quán)訪問外部地址,只讀緩存時緩存區(qū)無緩存,等。。。
            responseListener.onFailure(HttpCode.CODE_RESPONSE_ERROR, "有網(wǎng)但請求失敗包含:網(wǎng)絡(luò)未授權(quán)訪問外部地址,只讀緩存時緩存區(qū)無緩存,等。。。");

        } else{

            responseListener.onFailure(HttpCode.CODE_UNKNOW, "未知網(wǎng)絡(luò)錯誤");
        }
        responseListener.onFinish();
    }

END


框架已發(fā)布JitPack:
github開源地址:TBRetroift
歡迎Star me fork me
如果本篇內(nèi)容有錯或者在使用后有問題歡迎下方留言或者GitHub上提issue。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容