Retrofit + OkHttp +RxJava 網(wǎng)絡(luò)庫構(gòu)建及項(xiàng)目實(shí)踐

前言:

Retrofit是Square公司開發(fā)的一款針對(duì)Android網(wǎng)絡(luò)請(qǐng)求的框架闷营,Retrofit2底層基于OkHttp實(shí)現(xiàn)的烤黍,OkHttp現(xiàn)在已經(jīng)得到Google官方認(rèn)可,大量的app都采用OkHttp做網(wǎng)絡(luò)請(qǐng)求傻盟,其源碼詳見OkHttp Github速蕊。

RxJava 在 GitHub 主頁上的自我介紹是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一個(gè)在 Java VM 上使用可觀測(cè)的序列來組成異步的、基于事件的程序的庫)娘赴。RxJava在處理異步操作時(shí)规哲,能夠讓異步代碼異常簡(jiǎn)潔,且不會(huì)隨著程序邏輯的復(fù)雜性增加而丟失其簡(jiǎn)潔性诽表。同時(shí)Rxjava在涉及到操作的線程切換時(shí)也非常的簡(jiǎn)潔和方便唉锌。

這篇文章主要針對(duì)已對(duì)Retrofit 和RxJava有基本了解的Developer腥光,在OkHttp和RxJava結(jié)合使用時(shí),項(xiàng)目應(yīng)用中的普遍存在的一些問題的解決方案進(jìn)行介紹糊秆。Retrofit和RxJava 基本用法這里不再介紹武福,感興趣的童鞋請(qǐng)自行搜索或點(diǎn)擊文章最后的推薦鏈接查閱。項(xiàng)目中用到的Retrofit 和Rxjava版本和配置如下:

compile 'com.squareup.okhttp3:okhttp:3.8.0'
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
compile 'io.reactivex:rxjava:1.3.0'
compile 'io.reactivex:rxandroid:1.2.1'

在新項(xiàng)目中發(fā)現(xiàn)原來的網(wǎng)絡(luò)庫在使用Retrofit時(shí)痘番,是使用Retrofit的同步請(qǐng)求方式捉片,外層通過AsyncTask進(jìn)行線程異步。調(diào)用方式比較繁瑣和麻煩汞舱。后來決定重新做個(gè)網(wǎng)絡(luò)庫伍纫,就有了這篇文章。Retrofit本身提供同步和異步調(diào)用方式昂芜。

同步請(qǐng)求:

BookSearchResponse response =call.execute().body();
網(wǎng)絡(luò)請(qǐng)求需要在子線程中完成莹规,不能直接在UI線程執(zhí)行,不然會(huì)crash

異步請(qǐng)求:

call.enqueue(newCallback() {
@Override
publicvoid onResponse(Call call,Respons eresponse) {
     asyncText.setText("異步請(qǐng)求結(jié)果: "+response.body().books.get(0).altTitle);
}
@Override
publicvoid onFailure(Callcall, Throwable t) {

     }
});

異步請(qǐng)求相對(duì)同步請(qǐng)求更簡(jiǎn)便和快捷泌神,開發(fā)者只需要再onResponse和OnFailure中處理對(duì)應(yīng)回調(diào)即可良漱。但是這種回調(diào)方式本身也有不方便的地方。因?yàn)榛卣{(diào)直接是在UI線程欢际,如果在OnResponse中回調(diào)的數(shù)據(jù)還要進(jìn)行耗時(shí)操作母市,比如和數(shù)據(jù)庫中的數(shù)據(jù)對(duì)比,或者返回結(jié)果是圖片的Url 需要再次通過網(wǎng)絡(luò)請(qǐng)求得到網(wǎng)絡(luò)圖片损趋,上述回調(diào)的方式就需要再開線程來處理患久,而使用RxJava的話,其優(yōu)點(diǎn)在于異步操作和線程切換浑槽,我們就可以比較優(yōu)雅和輕松的解決上述問題蒋失。

網(wǎng)絡(luò)庫架構(gòu)圖如下:

網(wǎng)絡(luò)架構(gòu).png

先簡(jiǎn)要看下網(wǎng)絡(luò)請(qǐng)求配置:

public class OKHttpClientUtils {
    public static OkHttpClient sOkHttpClient;
    private static Converter.Factory sGsonConverterFactory = GsonConverterFactory.create();
    private static Converter.Factory sStringConverterFactory = StringConverterFactory.create();
    private static CallAdapter.Factory sRXJavaCallAdapterFactory =   
    RxJavaCallAdapterFactory.create();
    private static Context sContext; //這里的Context必須是applicationContext

    public static void init(CustomContext context) {
        if (sOkHttpClient == null) {
            sOkHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(30, TimeUnit.SECONDS)
                    .readTimeout(30, TimeUnit.SECONDS)
                    .cookieJar(new CommonCookieJar())
                    .addInterceptor(new CommonAppInterceptor())
                    .build();

            sContext = context.getAppContext().getApplicationContext();
        }
    }
    
    public static class CommonCookieJar implements CookieJar {
        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            Log.v("OKHttpClientUtils", "response cookieHeader---->" + cookies);
            CookieHelper.saveCookies(cookies);
        }

        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            Log.v("OKHttpClientUtils", "requestCookie---->" +                    
            CookieHelper.getCookieHeader(url.uri()));
            return CookieHelper.getCookieHeader(url.uri());
        }
    }

    public static class CommonAppInterceptor implements Interceptor {
       ...//處理公共請(qǐng)求參數(shù)統(tǒng)一添加
       ...//處理公共請(qǐng)求Header統(tǒng)一添加
    }
    
    public static <T> T createService(Class<T> clazz) {
        Retrofit retrofit =
                new Retrofit.Builder()
                        .client(sOkHttpClient)
                        .baseUrl(getAndroidHost(clazz))
                        .addConverterFactory(sStringConverterFactory)
                        .addConverterFactory(sGsonConverterFactory)
                        .addCallAdapterFactory(sRXJavaCallAdapterFactory)
                        .build();
        return retrofit.create(clazz);
    }

    /**
     * 獲取host  retrofit2 baseUrl 需要以 "/" 結(jié)尾
     */
    public static <T> String getAndroidHost(Class<T> clazz) {
       //通過注解拿到各個(gè)微服務(wù)配置的host
    }
}

上面顯示的OkHttpClientUtil中的各項(xiàng)配置下文會(huì)介紹。

本文將主要通過以下幾個(gè)方面進(jìn)行介紹:

  • 通用實(shí)體定義

  • 如何優(yōu)雅地處理服務(wù)器返回錯(cuò)誤碼及自定義異常

  • 簡(jiǎn)便的調(diào)用方式(滿足微服務(wù)多域名BaseUrl等)

  • Cookie本地保存及請(qǐng)求時(shí)添加統(tǒng)一處理

  • 通過攔截器實(shí)現(xiàn)get及post請(qǐng)求的公共參數(shù)及公共Header的統(tǒng)一添加

  • 如何優(yōu)雅地取消網(wǎng)絡(luò)請(qǐng)求回調(diào)的全局處理

1桐玻、通用實(shí)體定義:

public class StatusResponse<Result> implements Serializable {
    private static final long serialVersionUID = 6316903436640469387L;

    /**
     * code 取值  說明
     * 0    成功
     * < 0  通用錯(cuò)誤碼篙挽,與具體業(yè)務(wù)無關(guān)
     * > 0  業(yè)務(wù)錯(cuò)誤碼
     */
    public int code = 0;
    public String msg;
    public String errorMsg;

    /**
     * showType 說明
     * 0    Toast 形式
     * 1    Alert  形式
     */
    public int showType = -1;

    Result result;

    public boolean isOK() {
        return code == 0;
    }
}

客戶端跟服務(wù)器端定義的規(guī)則為,所有的請(qǐng)求數(shù)據(jù)包含code畸冲,msg嫉髓,errorMsg观腊,和showType邑闲。 Result泛型為各接口返回的數(shù)據(jù)。其中當(dāng)code==0 時(shí)為正常情況梧油,code<0 時(shí)客戶端需根據(jù)showType 及errorMsg分別用彈框或toast方式提示對(duì)應(yīng)錯(cuò)誤信息苫耸,code>0客戶端需要自行處理對(duì)應(yīng)情況。后續(xù)所有網(wǎng)絡(luò)請(qǐng)求返回?cái)?shù)據(jù)均按照StatusResponse<T>的形式返回?cái)?shù)據(jù)儡陨。

2褪子、如何優(yōu)雅地處理服務(wù)器返回錯(cuò)誤碼及自定義異常

因?yàn)樯厦嫣岬娇蛻舳诵枰y(tǒng)一處理code<0的異常情況量淌,所以想要用一種比較優(yōu)雅的方式來全局處理。查閱了相關(guān)資料嫌褪,發(fā)現(xiàn)基本是將code <0 作為一種自定義異常情況來處理呀枢。但是報(bào)出異常的方式有幾種。

一種做法是通過重寫GsonConverterFactory笼痛,在服務(wù)器數(shù)據(jù)進(jìn)行Gson轉(zhuǎn)化時(shí)裙秋,重寫GsonResponseBodyConverter 類。

class MyGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final Gson gson;
    private final Type type;

    MyGsonResponseBodyConverter(Gson gson, Type type) {
        this.gson = gson;
        this.type = type;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            String response = value.string();
            StatusResponse<T> resultResponse = JsonUtil.fromJson(response,type);
            //對(duì)返回碼進(jìn)行判斷缨伊,如果是0摘刑,便返回object
            if (resultResponse.code == 0) {
                return resultResponse.infos;
            } else {
                //拋出自定義服務(wù)器異常
                throw new ServerException(resultResponse.state, resultResponse.error);
            }
        }finally {
//            Utils.closeQuietly(reader);
        }
    }
}

在convert時(shí) resultResponse.code是否等于0來判斷是否拋出自定義的ServerException。但是我覺得這種方式需要重寫GsonConverterFactory GsonResponseBodyConverter 等相關(guān)類刻坊,在使用時(shí)還是有不安全性和不便捷性枷恕。所以還是選擇通過Rxjava的Map方式實(shí)現(xiàn)的code碼判斷和異常拋出。

服務(wù)器錯(cuò)誤碼統(tǒng)一處理及自定義異常.png

我們先來看調(diào)用的時(shí)候如何調(diào)用谭胚,可以先不用管MapTransformer 而只看call 方法里的內(nèi)容

public class MapTransformer<T> implements 
Observable.Transformer<StatusResponse<T>,StatusResponse<T>> {
@Override
public Observable<StatusResponse<T>> call(Observable<StatusResponse<T>> 
statusResponseObservable) {
    return statusResponseObservable.subscribeOn(Schedulers.io())
            .map(new ServerResultFunc<T>()) 
//              Instructs an ObservableSource to pass control to another ObservableSource
//              rather than invoking onError if it encounters an error.
            .onErrorResumeNext(new HttpResultFunc<StatusResponse<T>>())
            .observeOn(AndroidSchedulers.mainThread());
     }
}

主要包括這幾個(gè)類:

1)ServerResultFunc:

進(jìn)行Map操作的類徐块,主要是在進(jìn)行轉(zhuǎn)化的時(shí)候,通過判斷tStatusResponse.getCode()
是否<0 來決定是否拋出自定義的ServerException 異常灾而。

這里自己也思考了很久蛹锰,主要包括兩個(gè)問題。
一個(gè)問題是code >0 是否應(yīng)該作為異常處理绰疤,第二個(gè)問題是在進(jìn)行轉(zhuǎn)化的時(shí)候铜犬,是否應(yīng)該將StatusResponse去 掉,即 ServerResultFunc<T> implements Func1<StatusResponse<T>, T> 直接將T
而不是StatusResponse<T> 回調(diào)給OnNext(參數(shù)...) 作為回調(diào)參數(shù)轻庆,這兩個(gè)問題我們后面解答癣猾。

public class ServerResultFunc<T> implements Func1<StatusResponse<T>, StatusResponse<T>> {
    @Override
    public StatusResponse<T> call(StatusResponse<T> tStatusResponse) {
        if (tStatusResponse.getCode() < 0) {
            throw new ServerException(tStatusResponse.getCode(),tStatusResponse.getErrorMsg(),
                    tStatusResponse.getShowType());
        }
        return tStatusResponse;
    }
}

2)ServerException :

public class ServerException extends RuntimeException {

    private static final long serialVersionUID = 8484806560666715715L;
    private int code;
    private String errorMsg;
    private int showType = -1;

    public ServerException(int code, String msg,int showType) {
        this.code = code;
        this.errorMsg = msg;
        this.showType = showType;
    }

    public int getCode() {
        return code;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public int getShowType() {
        return showType;
    }
}

3)HttpResultFunc:

這個(gè)類主要是onErrorResumeNext時(shí)觸發(fā),作用是當(dāng)遇到error時(shí)不會(huì)直接觸發(fā)onError而是先走到HttpResultFunc call方法余爆,即在上面進(jìn)行Map時(shí)纷宇,ServerResultFunc中code <0 拋出ServerException時(shí),截獲這個(gè)exception 使其先到HttpResultFunc 的call方法中蛾方,通過ExceptionEngine.handleException(throwable)構(gòu)造我們的自定義的ApiException再將ApiException 交給OnError進(jìn)行回調(diào)像捶。

public class HttpResultFunc <T> implements Func1<Throwable, Observable<T>> {

    @Override
    public Observable<T> call(Throwable throwable) {
//      Returns an Observable that invokes an Observer's onError method when the Observer subscribes to it.
        return Observable.error(ExceptionEngine.handleException(throwable));
    }
}

4) ExceptionEngine :

public class ExceptionEngine {
    //對(duì)應(yīng)HTTP的狀態(tài)碼
    private static final int UNAUTHORIZED = 401;
    private static final int FORBIDDEN = 403;
    private static final int NOT_FOUND = 404;
    private static final int REQUEST_TIMEOUT = 408;
    private static final int INTERNAL_SERVER_ERROR = 500;
    private static final int BAD_GATEWAY = 502;
    private static final int SERVICE_UNAVAILABLE = 503;
    private static final int GATEWAY_TIMEOUT = 504;

    public static ApiException handleException(Throwable e){
        ApiException ex;
        if (e instanceof HttpException){             //HTTP錯(cuò)誤
            HttpException httpException = (HttpException) e;
            ex = new ApiException(e, ERROR.HTTP_ERROR);
            switch(httpException.code()){
                case UNAUTHORIZED:
                case FORBIDDEN:
                case NOT_FOUND:
                case REQUEST_TIMEOUT:
                case GATEWAY_TIMEOUT:
                case INTERNAL_SERVER_ERROR:
                case BAD_GATEWAY:
                case SERVICE_UNAVAILABLE:
                default:
                    ex.setErrorMsg("網(wǎng)絡(luò)錯(cuò)誤");  //均視為網(wǎng)絡(luò)錯(cuò)誤
                    break;
            }
            return ex;
        } else if (e instanceof ServerException){    //服務(wù)器返回的錯(cuò)誤
            ServerException resultException = (ServerException) e;
            ex = new ApiException(resultException, 
            resultException.getCode(),resultException.getShowType());
            ex.setSpecialException(true);
            ex.setErrorMsg(resultException.getErrorMsg());
            return ex;
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException){
            ex = new ApiException(e, ERROR.PARSE_ERROR);
            ex.setErrorMsg("解析錯(cuò)誤");            //均視為解析錯(cuò)誤
            return ex;
        }else if(e instanceof ConnectException){
            ex = new ApiException(e, ERROR.NETWORK_ERROR);
            ex.setErrorMsg("連接失敗");  //均視為網(wǎng)絡(luò)錯(cuò)誤
            return ex;
        }else {
            ex = new ApiException(e, ERROR.UNKNOWN);
            ex.setErrorMsg("未知錯(cuò)誤");          //未知錯(cuò)誤
            return ex;
        }
    }
}

5) ERROR:

/**
 * 與服務(wù)器約定好的異常 100000以上為客戶端定義的錯(cuò)誤碼code
 */
public class ERROR {
    /**
     * 未知錯(cuò)誤
     */
    public static final int UNKNOWN = 100000;
    /**
     * 解析錯(cuò)誤
     */
    public static final int PARSE_ERROR = 100001;
    /**
     * 網(wǎng)絡(luò)錯(cuò)誤
     */
    public static final int NETWORK_ERROR = 100002;
    /**
     * 協(xié)議出錯(cuò)
     */
    public static final int HTTP_ERROR = 100003;
}

6) ApiException:

 * code 取值  說明
 * 0    成功
 * < 0  通用錯(cuò)誤碼,與具體業(yè)務(wù)無關(guān)
 * > 0  業(yè)務(wù)錯(cuò)誤碼
 * <p>
 * showType 說明
 * 0    Toast 形式
 * 1    Alert  形式
 * msg 無意義桩砰。
 * <p>
 * code < 0拓春,框架處理,有errorMsg返回時(shí)亚隅,參考showType使用Toast或者Alert提示硼莽,無errorMsg時(shí),使用客戶端內(nèi)置的出錯(cuò)提示煮纵,區(qū)分紅包懂鸵、
 * 收銀臺(tái)偏螺、主站等不同系統(tǒng)內(nèi)置提示。code > 0匆光,交由業(yè)務(wù)邏輯處理套像,框架不處理。
 */
public class ApiException extends Exception {
    private static final long serialVersionUID = 4932302602588317500L;
    private boolean isSpecialException = false;
    private int code;
    private String errorMsg;
    private int showType = -1;

    public ApiException(Throwable throwable, int code) {
        super(throwable);
        this.code = code;
    }

    public ApiException(Throwable throwable, int code, int showType) {
        this(throwable, code);
        this.showType = showType;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public int getCode() {
        return code;
    }

    public int getShowType() {
        return showType;
    }

    public boolean isSpecialException() {
        return isSpecialException;
    }

    public void setSpecialException(boolean specialException) {
        isSpecialException = specialException;
    }
}

7) BaseSubscriber:

public abstract class BaseSubscriber<T> extends Subscriber<T> {

    public BaseSubscriber(CustomContext tag) {
        SubscriptionManager.getInstance().add(tag, this);
    }
    @Override
    public void onCompleted() {
    }

    @Override
    public void onError(Throwable e) {
        if (e instanceof ApiException) {
            ApiException apiException = (ApiException) e;

            int code = apiException.getCode();
            if (code < 0) {
                String errorMsg = apiException.getErrorMsg();
                int showType = apiException.getShowType();

                //為了和APP主項(xiàng)目解耦终息,采用EventBus發(fā)送消息給MainActivity來進(jìn)行對(duì)應(yīng)提示
                SubscriberEvent subscriberEvent = new SubscriberEvent(showType, errorMsg);
                EventBus.getDefault().post(subscriberEvent);

                Log.i("network", "onError--errorMsg->" + errorMsg);
                Log.i("network", "onError--code->" + apiException.getCode());
                Log.i("network", "onError--showType->" + showType);

                if (code == -200) {
                    EventBus.getDefault().post(new AuthEvent(false));
                }
            }

            onError((ApiException) e);

        } else {
            onError(new ApiException(e, ERROR.UNKNOWN));
            Log.i("network", "onError-otherError->" + e.toString());
        }
        Crashlytics.logException(e);
        Log.e("network", "exception-->" + e.toString());
    }

    /**
     * 錯(cuò)誤回調(diào)
     */
    protected abstract void onError(ApiException ex);
}

通過在BaseSubscriber的OnError中統(tǒng)一處理code <0的情況凉夯,而 code==0即正常情況,會(huì)回調(diào)到BaseSubscriber的onNext中采幌,而code>0也是走到onNext的回調(diào)劲够。

到這里統(tǒng)一錯(cuò)誤碼自定義異常處理就完成了,這里我們回到開頭提的兩個(gè)問題

第一 code >0是否應(yīng)該算作異常休傍,后來經(jīng)過實(shí)踐征绎,code>0 最好不算做異常,因?yàn)檫@里要客戶端根據(jù)不同的code做業(yè)務(wù)處理磨取,放在onNext處理比較方便人柿,而且onError中無法獲取StatusResponse<T>,也就無法滿足客戶端根據(jù)code處理各種業(yè)務(wù)的需求(各種業(yè)務(wù)中需要用到StatusResponse<T>的數(shù)據(jù))忙厌。

第二 在進(jìn)行轉(zhuǎn)化的時(shí)候凫岖,是否應(yīng)該將StatusResponse去掉,即 ServerResultFunc<T> implements Func1<StatusResponse<T>, T> 直接將T而不是StatusResponse<T> 回調(diào)給OnNext(參數(shù)...) 作為回調(diào)參數(shù)逢净。如果這樣做有個(gè)壞處是哥放,OnNext中無法拿到StatusResponse也就無法拿到StatusResponse.getCode()。這個(gè)跟我們code>0時(shí)客戶端自定義處理業(yè)務(wù)的需求相違背爹土,所以這里仍然保留StatusResponse甥雕。

3、簡(jiǎn)便的調(diào)用方式(滿足微服務(wù)多域名BaseUrl等):

因?yàn)轫?xiàng)目后臺(tái)采用微服務(wù)胀茵,每個(gè)模塊的接口域名都不一樣社露,即BaseUrl有多個(gè),所以這里需要?jiǎng)?chuàng)建多個(gè)Retrofit對(duì)象琼娘,并通過注解的方式峭弟,拿到develop(開發(fā)環(huán)境) alpha(測(cè)試環(huán)境)online(正式環(huán)境下配置的域名)

1)示例1 ActionCommon.java:

    @HOST(develop = API.Helper.HTTPS_PREFIX + API.Helper.HOST_APP_DEVELOP, 
    alpha = API.Helper.HTTPS_PREFIX + API.Helper.HOST_APP_ALPHA,
    online = API.Helper.HTTPS_PREFIX + API.Helper.HOST_APP_ONLINE)
    
    public interface ActionCommon {
        @GET("ooxx/user/userInfo.do")
        Observable<StatusResponse<UserInfoResponse>> getUserInfo();
    
        @GET("ooxx/index.do")
        Observable<StatusResponse<HallResponse>> hallIndex();
    
        @GET("/user/ooxx/list.do")
        Observable<StatusResponse<BaseListResponse<ListEntity>>> getList(@QueryMap Map<String, String> map);
    
        @GET("/user/ooxx/detail.do")
        Observable<StatusResponse<DetailEntity>> getDetail(@QueryMap Map<String, String> map);
    }

上面的注解HOST配置為這幾個(gè)接口對(duì)應(yīng)的微服務(wù)的域名,分別為develop(開發(fā)環(huán)境) alpha(測(cè)試環(huán)境)online(正式環(huán)境)下配置的域名)脱拼。

2)示例2 ActionBonus.java:

@HOST(develop = API.Helper.HTTPS_PREFIX + API.Helper.HOST_BONUS_DEVELOP,
alpha = API.Helper.HTTPS_PREFIX + API.Helper.HOST_BONUS_ALPHA,
online = API.Helper.HTTPS_PREFIX + API.Helper.HOST_BONUS_ONLINE)

public interface ActionBonus {
    @GET("/bonus/list.do")
    Observable<StatusResponse<BonusResponse>> list(@QueryMap Map<String, String> map);
}

3)API.java:

public class API {
/**
 * 主站服務(wù)
 */
public final static ActionCommon ACTION_COMMON = OKHttpClientUtils.createService(ActionCommon.class); 
/**
 * 紅包服務(wù)
 */
public final static ActionBonus ACTION_BONUS = OKHttpClientUtils.createService(ActionBonus.class);
/**
 * 用戶服務(wù)
 */
public final static ActionUser ACTION_USER = OKHttpClientUtils.createService(ActionUser.class);

public static class Helper {
    /**
     * 主站服務(wù)
     */
    static final String HOST_APP_DEVELOP = "develop.app." + DEVELOP_DOMAIN;
    static final String HOST_APP_ALPHA = "test.app." + ALPHA_DOMAIN;
    static final String HOST_APP_ONLINE = "app." + ONLINE_DOMAIN;
    /**
     * 紅包服務(wù)
     */
    static final String HOST_BONUS_DEVELOP = "develop.rp." + DEVELOP_DOMAIN;
    static final String HOST_BONUS_ALPHA = "test.rp." + ALPHA_DOMAIN;
    static final String HOST_BONUS_ONLINE = "bonus." + ONLINE_DOMAIN;
    
    ....
    
     }
}

createService中所做操作:

public static <T> T createService(Class<T> clazz) {
    Retrofit retrofit =
            new Retrofit.Builder()
                    .client(sOkHttpClient)
                    .baseUrl(getAndroidHost(clazz))
                    .addConverterFactory(sStringConverterFactory)
                    .addConverterFactory(sGsonConverterFactory)
                    .addCallAdapterFactory(sRXJavaCallAdapterFactory)
                    .build();
    return retrofit.create(clazz);
}

/**
 * 獲取host  retrofit2 baseUrl 需要以 "/" 結(jié)尾
 */
public static <T> String getAndroidHost(Class<T> clazz) {

    HOST host = clazz.getAnnotation(HOST.class);
    String trueHost;
    try {
        if (MiscUtils.isDevelop(sContext)) {
            // 開發(fā)環(huán)境
            trueHost = host.develop();
        } else if (MiscUtils.isAlpha(sContext)) {
            // 測(cè)試環(huán)境
            trueHost = host.alpha();
        } else {
            // 線上環(huán)境
            trueHost = host.online();
        }
    } catch (Exception e) {
        // 有異常默認(rèn)返回線上地址
        e.printStackTrace();
        trueHost = host.online();
    }
    return trueHost + "/";
}

下面看個(gè)具體調(diào)用實(shí)例:

API.ACTION_COMMON = OKHttpClientUtils.createService(ActionCommon.class);

public static Observable<StatusResponse<DetailEntity>> getDetail(String pid, String Id) {
    Map<String, String> params = new HashMap<String,String>();
//        Map<String,String> params=new HashMap<String, String>();
        params.put("pid",pid);
        params.put("id",Id);
        return API.ACTION_COMMON.getDetail(params)
                .compose(new MapTransformer<DetailEntity>());
    }

getDetail(pid瞒瘸,id).subscribe(new BaseSubscriber<StatusResponse<DetailEntity>>(this){

        @Override
        public void onNext(StatusResponse<DetailEntity> data) {
            DetailEntity detailEntity=data.getResult();
            ...
        }
        
        @Override
        protected void onError(ApiException ex) {
            ...
        }
    });

通過getDetail(pid,id) 即可完成該接口的網(wǎng)絡(luò)請(qǐng)求挪拟。當(dāng)然上述的compose方法只是目前項(xiàng)目中比較普遍的調(diào)用方式挨务,如果你在拿到Observable<StatusResponse<DetailEntity>>需要進(jìn)行其他的map flatmap等操作的話击你,可以自己實(shí)現(xiàn)對(duì)應(yīng)方法的調(diào)用玉组,不過需要處理MapTransformer中對(duì)服務(wù)器錯(cuò)誤碼自定義異常的處理操作谎柄,即(只是舉個(gè)示例)

API.ACTION_COMMON.getDetail(params).subscribeOn(Schedulers.io())
                .map(new ServerResultFunc<T>())
                ...
                .map(...)
                ...
                .flatMap(...)
                .onErrorResumeNext(new HttpResultFunc<StatusResponse<T>>())
                .observeOn(AndroidSchedulers.mainThread());

4.Cookie本地保存及請(qǐng)求時(shí)添加統(tǒng)一處理

Cookie本地保存及請(qǐng)求添加.png
new OkHttpClient.Builder().cookieJar(new CommonCookieJar())
    public static class CommonCookieJar implements CookieJar {
        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            Log.v("OKHttpClientUtils", "response cookieHeader---->" + cookies);
            CookieHelper.saveCookies(cookies);
        }
    
        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            Log.v("OKHttpClientUtils", "requestCookie---->" +                    
            CookieHelper.getCookieHeader(url.uri()));
            return CookieHelper.getCookieHeader(url.uri());
        }
    }

getCookie():

Cookie.Builder build = new Cookie.Builder();
build.name(savedCookieName);
build.value(sp.getString(savedCookieName));
build.domain(API.Helper.getCurrentDomain(context.getAppContext()));
List.add(build.build())
...

saveCookie():

SharedPreference.putString(cookieName,cookieValue);
...

說明:
saveFromResponse(HttpUrl url, List<Cookie> cookies) 中 通過CookieHelper.saveCookies(cookies),
將后臺(tái)接口返回的cookie保存在本地,并每次更新(客戶端本地加了一個(gè)cookie的白名單列表惯雳,只有在白名單中,才會(huì)將對(duì)應(yīng)cookie存儲(chǔ)在本地)
loadForRequest(HttpUrl url)中朝巫,調(diào)用CookieHelper.getCookieHeader(url.uri()),這里主要是將本地?cái)?shù)據(jù)如token id等數(shù)據(jù) 構(gòu)造成Retrofit2的Cookie石景,然后組裝成List<Cookie>劈猿,在loadForRequest時(shí)傳給后臺(tái)服務(wù)器。

5.通過攔截器實(shí)現(xiàn)get及post請(qǐng)求的公共參數(shù)及Header的統(tǒng)一添加

公共參數(shù)和Header的統(tǒng)一添加潮孽,是通過OKHttp的攔截器實(shí)現(xiàn)揪荣。攔截器是OKHttp提供的一種強(qiáng)大的機(jī)制,可以監(jiān)視、重寫和重試調(diào)用往史。很多功能比如緩存數(shù)據(jù)仗颈,接口請(qǐng)求的加密解密等,均可以通過攔截器實(shí)現(xiàn)椎例。其基礎(chǔ)概念和用法可以參考:Okhttp-wiki 之 Interceptors 攔截器

new OkHttpClient.Builder().addInterceptor(new CommonAppInterceptor());

public static class CommonAppInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            String token = null;
            try {
                token =   
                SharedPrefsManager.getInstance(BaseApplication.getContext()).getString(SharedPre
                fsManager.TOKEN);
            } catch (BaseException e) {
                e.printStackTrace();
            }
            Request request = chain.request();
            Request.Builder newBuilder = request.newBuilder();
            // get請(qǐng)求
            if (request.method().equals("GET")) {
                // GET 請(qǐng)求
                HttpUrl.Builder builder = request.url().newBuilder();
                builder.setQueryParameter("t", StringUtil.random());
                if (token != null) {
                    builder.setQueryParameter(AuthProxy.Token, token);
                }
                HttpUrl httpUrl = builder.build();
                newBuilder.url(httpUrl);
               
            } // post請(qǐng)求
            else if (request.method().equals("POST")) {
                //Form表單
                if (request.body() instanceof FormBody) {
                    FormBody.Builder bodyBuilder = new FormBody.Builder();

                    FormBody oldFormBody = (FormBody) request.body();
                    //把原來的參數(shù)添加到新的構(gòu)造器挨决,(因?yàn)闆]找到直接添加,所以就new新的)
                    for (int i = 0; i < oldFormBody.size(); i++) {
                        bodyBuilder.addEncoded(oldFormBody.encodedName(i), oldFormBody.encodedValue(i));
                    }
                    bodyBuilder.addEncoded("t", StringUtil.random());
                    if (token != null) {
                        bodyBuilder.addEncoded(AuthProxy.TOKEN, token);
                    }
                    newBuilder.post(bodyBuilder.build());
                }
                //MultipartBody
                else if (request.body() instanceof MultipartBody) {
                    MultipartBody.Builder multipartBuilder = new 
                    MultipartBody.Builder().setType(MultipartBody.FORM);
                    List<MultipartBody.Part> oldParts = ((MultipartBody) 
                    request.body()).parts();
                    if (oldParts != null && oldParts.size() > 0) {
                        for (MultipartBody.Part part : oldParts) {
                            multipartBuilder.addPart(part);
                        }
                    }

                    multipartBuilder.addFormDataPart("t", StringUtil.random());
                    if (token != null) {
                        multipartBuilder.addFormDataPart(AuthProxy.TOKEN, token);
                    }
                    newBuilder.post(multipartBuilder.build());
                }
            }

            //公共Header的統(tǒng)一添加
            Header[] headers = new Header[]{HeaderManager.getUAHeader(sContext),
                    HeaderManager.getModifiedUAHeader(sContext)};
            for (Header head : headers) {
                newBuilder.addHeader(head.getName(), head.getValue());
            }

            request = newBuilder.build();

            //The network interceptor's Chain has a non-null Connection that can be used to interrogate
            // the IP address and TLS configuration that were used to connect to the webserver.
            //應(yīng)用攔截器的chain.connection(), request.headers() 為空订歪,網(wǎng)絡(luò)攔截器不為空
            long t1 = System.nanoTime();
            Log.d("OKHttpClientUtils", String.format("CommonAppInterceptor---->Sending request 
            %s on %s%n%s",request.url(), chain.connection(), request.headers()));

            Response response = chain.proceed(request);

            long t2 = System.nanoTime();
            Log.d("OKHttpClientUtils", String.format("CommonAppInterceptor---->Received response
            for %s in %.1fms%n%s",response.request().url(), (t2 - t1) / 1e6d, 
            response.headers()));

            return response;
        }
    }

get請(qǐng)求比較簡(jiǎn)單脖祈,就是將公共請(qǐng)求參數(shù)加入到請(qǐng)求的url中,這里是通過request.url().newBuilder().setQueryParameter(key刷晋,value)的方式添加盖高,而不是addQueryParameter,add的話,如果外部調(diào)用時(shí)也有加這個(gè)參數(shù)眼虱,就會(huì)出現(xiàn)請(qǐng)求參數(shù)添加了多個(gè)的情況或舞,而set的話,可以直接替換(替換是不會(huì)造成問題的)蒙幻。

Post請(qǐng)求需要區(qū)分幾種情況映凳,看是以表單提交方式FormBody(目前項(xiàng)目post請(qǐng)求基本是這種),還是以MultipartBody(上傳文件邮破,圖片等比較常用)诈豌,當(dāng)然如果還有其他提交方式,比如流數(shù)據(jù)提交抒和,也是可以在攔截器統(tǒng)一處理的矫渔,因?yàn)轫?xiàng)目暫未用到,這里不再贅述(當(dāng)然這種情況比較少見摧莽,也可以在外部調(diào)用時(shí)由調(diào)用者自行添加而不是在攔截器中統(tǒng)一添加)庙洼。

添加公共Hearder Request.newBuilder().addHeader(key,value);

6.如何優(yōu)雅地取消網(wǎng)絡(luò)請(qǐng)求回調(diào)的全局處理

作為Android開發(fā)者比較容易碰到的一個(gè)問題就是,在一個(gè)頁面比如Actiivty,如果這個(gè)頁面還在進(jìn)行網(wǎng)絡(luò)請(qǐng)求,但是用戶又要退出這個(gè)頁面油够,那么該如何取消這個(gè)網(wǎng)絡(luò)請(qǐng)求呢蚁袭,其實(shí)一般來說,異步操作一旦進(jìn)行石咬,是無法取消的揩悄,所以我們這里只是取消網(wǎng)絡(luò)請(qǐng)求回調(diào),而不是取消網(wǎng)絡(luò)請(qǐng)求鬼悠。RxJava的訂閱機(jī)制可以通過Subscription.unsubscribe取消訂閱删性,來取消網(wǎng)絡(luò)請(qǐng)求回調(diào),這樣就不會(huì)出現(xiàn)網(wǎng)絡(luò)請(qǐng)求正在進(jìn)行焕窝,頁面銷毀蹬挺,請(qǐng)求完成回調(diào)到OnNext或onError(UI線程),造成空指針或內(nèi)存泄漏的問題它掂。

基本思路就是汗侵,全局單例中,有個(gè)Map<Tag, List<Subscription>> Tag可以理解為各個(gè)頁面群发,List<Subscription>為每個(gè)頁面里網(wǎng)絡(luò)請(qǐng)求的訂閱關(guān)系晰韵,在該頁面銷毀時(shí),遍歷List<Subscription>熟妓,如果Subscription還未被取消訂閱雪猪,就執(zhí)行取消訂閱操作

取消網(wǎng)絡(luò)請(qǐng)求回調(diào)的全局處理.png

上文提到過的BaseSubscriber

public abstract class BaseSubscriber<T> extends Subscriber<T> {

public BaseSubscriber(CustomContext tag) {
    SubscriptionManager.getInstance().add(tag, this);
}

@Override
public void onCompleted() {
}

@Override
public void onError(Throwable e) {
    if (e instanceof ApiException) {
        ...相關(guān)處理
        }
        onError((ApiException) e);

    } else {
        onError(new ApiException(e, ERROR.UNKNOWN));
        Log.i("network", "onError-otherError->" + e.toString());
    }
    Crashlytics.logException(e);
    Log.e("network", "exception-->" + e.toString());
}
/**
 * 錯(cuò)誤回調(diào)
 */
protected abstract void onError(ApiException ex);

}

構(gòu)造函數(shù)中 添加 SubscriptionManager.getInstance().add(tag, this);

public interface ISubscription<T> {

void add(T tag, Subscription subscription);

void remove(T tag);

void removeAll();

void cancel(T tag);

void cancelAll();

String getName(T tag);

}

public class SubscriptionManager<T> implements ISubscription<T> {
    
    private Map<Object, List<Subscription>> mMap = new HashMap<>();
    
    private static SubscriptionManager sSubscriptionManager;
    
    public SubscriptionManager() {
    }

public static synchronized SubscriptionManager getInstance() {
    if (sSubscriptionManager == null) {
        sSubscriptionManager = new SubscriptionManager();
    }
    return sSubscriptionManager;
}

@Override
public void add(T tag, Subscription subscription) {
    List<Subscription> perPageList = mMap.get(tag);
    if (perPageList == null) {
        perPageList = new ArrayList<>();
        mMap.put(tag, perPageList);
    }

    perPageList.add(subscription);
    mMap.put(tag, perPageList);

}

@Override
public void remove(T tag) {
    if (!mMap.isEmpty()) {
        List<Subscription> perPageList = mMap.get(tag);
        if (perPageList != null && perPageList.size() > 0) {
            mMap.remove(tag);
        }
    }

}

@Override
public void removeAll() {
    if (!mMap.isEmpty()) {
        mMap.clear();
    }
}

@Override
public void cancel(T tag) {
    if (!mMap.isEmpty()) {
        List<Subscription> perPageList = mMap.get(tag);
        if (perPageList != null && perPageList.size() > 0) {
            for (Subscription subscription : perPageList) {
                if (subscription != null && !subscription.isUnsubscribed()) {
                    subscription.unsubscribe();
                }
            }
            Log.d("SubscriptionManager","tag--->"+tag);
            Log.d("SubscriptionManager","perPageList--->"+perPageList.size());
            mMap.remove(tag);
        }
    }

}

@Override
public void cancelAll() {
    if (!mMap.isEmpty()) {
        Set<Object> keys = mMap.keySet();
        for (Object apiKey : keys) {
            cancel((T)apiKey);
        }
    }
}

@Override
public String getName(T tag) {
    return tag.getClass().getName();
}

}

網(wǎng)絡(luò)請(qǐng)求調(diào)用即為

public static Observable<StatusResponse<BaseListResponse<ResultListEntity>>> 
    getResultList(String offset, String pageSize) {
            Map<String, String> params = new HashMap<String,String>();
            params.put("offset", offset);
            params.put("pageSize", pageSize);
            return API.ACTION.getResultList(params)
                    .compose(new MapTransformer<BaseListResponse<ResultListEntity>>());
        }
        
getResultList(mOffset, String.valueOf(DEFAULT_PAGE_SIZE)).subscribe(
new BaseSubscriber<StatusResponse<BaseListResponse<ResultListEntity>>>(this) {
             @Override
              protected void onError(ApiException ex) {
                   onDataFail(ex);
              }
             @Override
             public void onNext(StatusResponse<BaseListResponse<ResultListEntity>> data) {
                   onDataSuccess(data.getResult());
             }
       });

在BaseActivity的onDestroy(),BaseFragment的OnDestroyView()中調(diào)用SubscriptionManager.getInstance().cancel(this);即可起愈。
其中只恨,上文中的CustomContext 可以理解為任意的一個(gè)接口,BaseActivity BaseFragment BaseContentView(自定義View)等抬虽,所有需要全局取消網(wǎng)絡(luò)請(qǐng)求的類官觅,均需要實(shí)現(xiàn)這個(gè)接口。實(shí)現(xiàn)該接口的類阐污,需要在其生命周期結(jié)束時(shí)休涤,執(zhí)行SubscriptionManager.getInstance().cancel(this);進(jìn)行訂閱關(guān)系的判斷和取消訂閱操作笛辟。

結(jié)語:

本文主要講述在使用Retrofit和RxJava做網(wǎng)絡(luò)請(qǐng)求庫時(shí)功氨,從基礎(chǔ)網(wǎng)絡(luò)配置,通用實(shí)體定義手幢,Cookie相關(guān)處理捷凄,調(diào)用方式優(yōu)化,服務(wù)器錯(cuò)誤碼及自定義異常的全局處理围来,公共請(qǐng)求參數(shù)Header的統(tǒng)一添加跺涤,全局取消網(wǎng)絡(luò)請(qǐng)求回調(diào)等項(xiàng)目實(shí)踐中容易遇到的問題的一些解決方案匈睁。還有其他如添加緩存,接口加密解密等比較常見的場(chǎng)景后續(xù)可以擴(kuò)展桶错。

因時(shí)間關(guān)系文章難免有疏漏航唆,歡迎提出指正,謝謝牛曹。同時(shí)對(duì)RxJava和Retrofit感興趣的童鞋可以參考以下鏈接:

1佛点、Retrofit用法詳解
2醇滥、給 Android 開發(fā)者的 RxJava 詳解
3黎比、Okhttp-wiki 之 Interceptors 攔截器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鸳玩,隨后出現(xiàn)的幾起案子阅虫,更是在濱河造成了極大的恐慌,老刑警劉巖不跟,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颓帝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窝革,警方通過查閱死者的電腦和手機(jī)购城,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虐译,“玉大人瘪板,你說我怎么就攤上這事∑岱蹋” “怎么了侮攀?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)厢拭。 經(jīng)常有香客問我兰英,道長(zhǎng),這世上最難降的妖魔是什么供鸠? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任畦贸,我火速辦了婚禮,結(jié)果婚禮上楞捂,老公的妹妹穿的比我還像新娘家制。我一直安慰自己,他們只是感情好泡一,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布颤殴。 她就那樣靜靜地躺著,像睡著了一般鼻忠。 火紅的嫁衣襯著肌膚如雪涵但。 梳的紋絲不亂的頭發(fā)上杈绸,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音矮瘟,去河邊找鬼瞳脓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛澈侠,可吹牛的內(nèi)容都是我干的劫侧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼哨啃,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼烧栋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拳球,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤审姓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后祝峻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魔吐,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年莱找,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酬姆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奥溺,死狀恐怖辞色,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谚赎,我是刑警寧澤淫僻,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站壶唤,受9級(jí)特大地震影響雳灵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜闸盔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一悯辙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧迎吵,春花似錦躲撰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔫巩,卻和暖如春谆棱,著一層夾襖步出監(jiān)牢的瞬間快压,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工垃瞧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔫劣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓个从,卻偏偏與公主長(zhǎng)得像脉幢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嗦锐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容