前言:
Retrofit是Square公司開發(fā)的一款針對Android網(wǎng)絡(luò)請求的框架,Retrofit2底層基于OkHttp實現(xiàn)的,OkHttp現(xiàn)在已經(jīng)得到Google官方認可,大量的app都采用OkHttp做網(wǎng)絡(luò)請求,其源碼詳見OkHttp Github。
RxJava 在 GitHub 主頁上的自我介紹是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一個在 Java VM 上使用可觀測的序列來組成異步的、基于事件的程序的庫)。RxJava在處理異步操作時,能夠讓異步代碼異常簡潔,且不會隨著程序邏輯的復(fù)雜性增加而丟失其簡潔性。同時Rxjava在涉及到操作的線程切換時也非常的簡潔和方便。
這篇文章主要針對已對Retrofit 和RxJava有基本了解的Developer,在OkHttp和RxJava結(jié)合使用時,項目應(yīng)用中的普遍存在的一些問題的解決方案進行介紹。Retrofit和RxJava 基本用法這里不再介紹,感興趣的童鞋請自行搜索或點擊文章最后的推薦鏈接查閱。項目中用到的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'
在新項目中發(fā)現(xiàn)原來的網(wǎng)絡(luò)庫在使用Retrofit時,是使用Retrofit的同步請求方式,外層通過AsyncTask進行線程異步。調(diào)用方式比較繁瑣和麻煩。后來決定重新做個網(wǎng)絡(luò)庫,就有了這篇文章。Retrofit本身提供同步和異步調(diào)用方式。
同步請求:
BookSearchResponse response =call.execute().body();
網(wǎng)絡(luò)請求需要在子線程中完成,不能直接在UI線程執(zhí)行,不然會crash
異步請求:
call.enqueue(newCallback() {
@Override
publicvoid onResponse(Call call,Respons eresponse) {
asyncText.setText("異步請求結(jié)果: "+response.body().books.get(0).altTitle);
}
@Override
publicvoid onFailure(Callcall, Throwable t) {
}
});
異步請求相對同步請求更簡便和快捷,開發(fā)者只需要再onResponse和OnFailure中處理對應(yīng)回調(diào)即可。但是這種回調(diào)方式本身也有不方便的地方。因為回調(diào)直接是在UI線程,如果在OnResponse中回調(diào)的數(shù)據(jù)還要進行耗時操作,比如和數(shù)據(jù)庫中的數(shù)據(jù)對比,或者返回結(jié)果是圖片的Url 需要再次通過網(wǎng)絡(luò)請求得到網(wǎng)絡(luò)圖片,上述回調(diào)的方式就需要再開線程來處理,而使用RxJava的話,其優(yōu)點在于異步操作和線程切換,我們就可以比較優(yōu)雅和輕松的解決上述問題。
網(wǎng)絡(luò)庫架構(gòu)圖如下:
先簡要看下網(wǎng)絡(luò)請求配置:
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 {
...//處理公共請求參數(shù)統(tǒ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) {
//通過注解拿到各個微服務(wù)配置的host
}
}
上面顯示的OkHttpClientUtil中的各項配置下文會介紹。
本文將主要通過以下幾個方面進行介紹:
-
通用實體定義
-
如何優(yōu)雅地處理服務(wù)器返回錯誤碼及自定義異常
-
簡便的調(diào)用方式(滿足微服務(wù)多域名BaseUrl等)
-
Cookie本地保存及請求時添加統(tǒng)一處理
-
通過攔截器實現(xiàn)get及post請求的公共參數(shù)及公共Header的統(tǒng)一添加
-
如何優(yōu)雅地取消網(wǎng)絡(luò)請求回調(diào)的全局處理
1、通用實體定義:
public class StatusResponse<Result> implements Serializable {
private static final long serialVersionUID = 6316903436640469387L;
/**
* code 取值 說明
* 0 成功
* < 0 通用錯誤碼,與具體業(yè)務(wù)無關(guān)
* > 0 業(yè)務(wù)錯誤碼
*/
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ī)則為,所有的請求數(shù)據(jù)包含code,msg,errorMsg,和showType。 Result泛型為各接口返回的數(shù)據(jù)。其中當(dāng)code==0 時為正常情況,code<0 時客戶端需根據(jù)showType 及errorMsg分別用彈框或toast方式提示對應(yīng)錯誤信息,code>0客戶端需要自行處理對應(yīng)情況。后續(xù)所有網(wǎng)絡(luò)請求返回數(shù)據(jù)均按照StatusResponse<T>的形式返回數(shù)據(jù)。
2、如何優(yōu)雅地處理服務(wù)器返回錯誤碼及自定義異常
因為上面提到客戶端需要統(tǒng)一處理code<0的異常情況,所以想要用一種比較優(yōu)雅的方式來全局處理。查閱了相關(guān)資料,發(fā)現(xiàn)基本是將code <0 作為一種自定義異常情況來處理。但是報出異常的方式有幾種。
一種做法是通過重寫GsonConverterFactory,在服務(wù)器數(shù)據(jù)進行Gson轉(zhuǎn)化時,重寫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);
//對返回碼進行判斷,如果是0,便返回object
if (resultResponse.code == 0) {
return resultResponse.infos;
} else {
//拋出自定義服務(wù)器異常
throw new ServerException(resultResponse.state, resultResponse.error);
}
}finally {
// Utils.closeQuietly(reader);
}
}
}
在convert時 resultResponse.code是否等于0來判斷是否拋出自定義的ServerException。但是我覺得這種方式需要重寫GsonConverterFactory GsonResponseBodyConverter 等相關(guān)類,在使用時還是有不安全性和不便捷性。所以還是選擇通過Rxjava的Map方式實現(xiàn)的code碼判斷和異常拋出。
我們先來看調(diào)用的時候如何調(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());
}
}
主要包括這幾個類:
1)ServerResultFunc:
進行Map操作的類,主要是在進行轉(zhuǎn)化的時候,通過判斷tStatusResponse.getCode()
是否<0 來決定是否拋出自定義的ServerException 異常。
這里自己也思考了很久,主要包括兩個問題。
一個問題是code >0 是否應(yīng)該作為異常處理,第二個問題是在進行轉(zhuǎn)化的時候,是否應(yīng)該將StatusResponse去 掉,即 ServerResultFunc<T> implements Func1<StatusResponse<T>, T> 直接將T
而不是StatusResponse<T> 回調(diào)給OnNext(參數(shù)...) 作為回調(diào)參數(shù),這兩個問題我們后面解答。
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:
這個類主要是onErrorResumeNext時觸發(fā),作用是當(dāng)遇到error時不會直接觸發(fā)onError而是先走到HttpResultFunc call方法,即在上面進行Map時,ServerResultFunc中code <0 拋出ServerException時,截獲這個exception 使其先到HttpResultFunc 的call方法中,通過ExceptionEngine.handleException(throwable)構(gòu)造我們的自定義的ApiException再將ApiException 交給OnError進行回調(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 {
//對應(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錯誤
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ò)錯誤"); //均視為網(wǎng)絡(luò)錯誤
break;
}
return ex;
} else if (e instanceof ServerException){ //服務(wù)器返回的錯誤
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("解析錯誤"); //均視為解析錯誤
return ex;
}else if(e instanceof ConnectException){
ex = new ApiException(e, ERROR.NETWORK_ERROR);
ex.setErrorMsg("連接失敗"); //均視為網(wǎng)絡(luò)錯誤
return ex;
}else {
ex = new ApiException(e, ERROR.UNKNOWN);
ex.setErrorMsg("未知錯誤"); //未知錯誤
return ex;
}
}
}
5) ERROR:
/**
* 與服務(wù)器約定好的異常 100000以上為客戶端定義的錯誤碼code
*/
public class ERROR {
/**
* 未知錯誤
*/
public static final int UNKNOWN = 100000;
/**
* 解析錯誤
*/
public static final int PARSE_ERROR = 100001;
/**
* 網(wǎng)絡(luò)錯誤
*/
public static final int NETWORK_ERROR = 100002;
/**
* 協(xié)議出錯
*/
public static final int HTTP_ERROR = 100003;
}
6) ApiException:
* code 取值 說明
* 0 成功
* < 0 通用錯誤碼,與具體業(yè)務(wù)無關(guān)
* > 0 業(yè)務(wù)錯誤碼
* <p>
* showType 說明
* 0 Toast 形式
* 1 Alert 形式
* msg 無意義。
* <p>
* code < 0,框架處理,有errorMsg返回時,參考showType使用Toast或者Alert提示,無errorMsg時,使用客戶端內(nèi)置的出錯提示,區(qū)分紅包、
* 收銀臺、主站等不同系統(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主項目解耦,采用EventBus發(fā)送消息給MainActivity來進行對應(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());
}
/**
* 錯誤回調(diào)
*/
protected abstract void onError(ApiException ex);
}
通過在BaseSubscriber的OnError中統(tǒng)一處理code <0的情況,而 code==0即正常情況,會回調(diào)到BaseSubscriber的onNext中,而code>0也是走到onNext的回調(diào)。
到這里統(tǒng)一錯誤碼自定義異常處理就完成了,這里我們回到開頭提的兩個問題
第一 code >0是否應(yīng)該算作異常,后來經(jīng)過實踐,code>0 最好不算做異常,因為這里要客戶端根據(jù)不同的code做業(yè)務(wù)處理,放在onNext處理比較方便,而且onError中無法獲取StatusResponse<T>,也就無法滿足客戶端根據(jù)code處理各種業(yè)務(wù)的需求(各種業(yè)務(wù)中需要用到StatusResponse<T>的數(shù)據(jù))。
第二 在進行轉(zhuǎn)化的時候,是否應(yīng)該將StatusResponse去掉,即 ServerResultFunc<T> implements Func1<StatusResponse<T>, T> 直接將T而不是StatusResponse<T> 回調(diào)給OnNext(參數(shù)...) 作為回調(diào)參數(shù)。如果這樣做有個壞處是,OnNext中無法拿到StatusResponse也就無法拿到StatusResponse.getCode()。這個跟我們code>0時客戶端自定義處理業(yè)務(wù)的需求相違背,所以這里仍然保留StatusResponse。
3、簡便的調(diào)用方式(滿足微服務(wù)多域名BaseUrl等):
因為項目后臺采用微服務(wù),每個模塊的接口域名都不一樣,即BaseUrl有多個,所以這里需要創(chuàng)建多個Retrofit對象,并通過注解的方式,拿到develop(開發(fā)環(huán)境) alpha(測試環(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配置為這幾個接口對應(yīng)的微服務(wù)的域名,分別為develop(開發(fā)環(huán)境) alpha(測試環(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)) {
// 測試環(huán)境
trueHost = host.alpha();
} else {
// 線上環(huán)境
trueHost = host.online();
}
} catch (Exception e) {
// 有異常默認返回線上地址
e.printStackTrace();
trueHost = host.online();
}
return trueHost + "/";
}
下面看個具體調(diào)用實例:
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ò)請求。當(dāng)然上述的compose方法只是目前項目中比較普遍的調(diào)用方式,如果你在拿到Observable<StatusResponse<DetailEntity>>需要進行其他的map flatmap等操作的話,可以自己實現(xiàn)對應(yīng)方法的調(diào)用,不過需要處理MapTransformer中對服務(wù)器錯誤碼自定義異常的處理操作,即(只是舉個示例)
API.ACTION_COMMON.getDetail(params).subscribeOn(Schedulers.io())
.map(new ServerResultFunc<T>())
...
.map(...)
...
.flatMap(...)
.onErrorResumeNext(new HttpResultFunc<StatusResponse<T>>())
.observeOn(AndroidSchedulers.mainThread());
4.Cookie本地保存及請求時添加統(tǒng)一處理
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),
將后臺接口返回的cookie保存在本地,并每次更新(客戶端本地加了一個cookie的白名單列表,只有在白名單中,才會將對應(yīng)cookie存儲在本地)
loadForRequest(HttpUrl url)中,調(diào)用CookieHelper.getCookieHeader(url.uri()),這里主要是將本地數(shù)據(jù)如token id等數(shù)據(jù) 構(gòu)造成Retrofit2的Cookie,然后組裝成List<Cookie>,在loadForRequest時傳給后臺服務(wù)器。
5.通過攔截器實現(xiàn)get及post請求的公共參數(shù)及Header的統(tǒng)一添加
公共參數(shù)和Header的統(tǒng)一添加,是通過OKHttp的攔截器實現(xiàn)。攔截器是OKHttp提供的一種強大的機制,可以監(jiān)視、重寫和重試調(diào)用。很多功能比如緩存數(shù)據(jù),接口請求的加密解密等,均可以通過攔截器實現(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請求
if (request.method().equals("GET")) {
// GET 請求
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請求
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)造器,(因為沒找到直接添加,所以就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請求比較簡單,就是將公共請求參數(shù)加入到請求的url中,這里是通過request.url().newBuilder().setQueryParameter(key,value)的方式添加,而不是addQueryParameter,add的話,如果外部調(diào)用時也有加這個參數(shù),就會出現(xiàn)請求參數(shù)添加了多個的情況,而set的話,可以直接替換(替換是不會造成問題的)。
Post請求需要區(qū)分幾種情況,看是以表單提交方式FormBody(目前項目post請求基本是這種),還是以MultipartBody(上傳文件,圖片等比較常用),當(dāng)然如果還有其他提交方式,比如流數(shù)據(jù)提交,也是可以在攔截器統(tǒng)一處理的,因為項目暫未用到,這里不再贅述(當(dāng)然這種情況比較少見,也可以在外部調(diào)用時由調(diào)用者自行添加而不是在攔截器中統(tǒng)一添加)。
添加公共Hearder Request.newBuilder().addHeader(key,value);
6.如何優(yōu)雅地取消網(wǎng)絡(luò)請求回調(diào)的全局處理
作為Android開發(fā)者比較容易碰到的一個問題就是,在一個頁面比如Actiivty,如果這個頁面還在進行網(wǎng)絡(luò)請求,但是用戶又要退出這個頁面,那么該如何取消這個網(wǎng)絡(luò)請求呢,其實一般來說,異步操作一旦進行,是無法取消的,所以我們這里只是取消網(wǎng)絡(luò)請求回調(diào),而不是取消網(wǎng)絡(luò)請求。RxJava的訂閱機制可以通過Subscription.unsubscribe取消訂閱,來取消網(wǎng)絡(luò)請求回調(diào),這樣就不會出現(xiàn)網(wǎng)絡(luò)請求正在進行,頁面銷毀,請求完成回調(diào)到OnNext或onError(UI線程),造成空指針或內(nèi)存泄漏的問題。
基本思路就是,全局單例中,有個Map<Tag, List<Subscription>> Tag可以理解為各個頁面,List<Subscription>為每個頁面里網(wǎng)絡(luò)請求的訂閱關(guān)系,在該頁面銷毀時,遍歷List<Subscription>,如果Subscription還未被取消訂閱,就執(zhí)行取消訂閱操作
上文提到過的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());
}
/**
* 錯誤回調(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ò)請求調(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 可以理解為任意的一個接口,BaseActivity BaseFragment BaseContentView(自定義View)等,所有需要全局取消網(wǎng)絡(luò)請求的類,均需要實現(xiàn)這個接口。實現(xiàn)該接口的類,需要在其生命周期結(jié)束時,執(zhí)行SubscriptionManager.getInstance().cancel(this);進行訂閱關(guān)系的判斷和取消訂閱操作。
結(jié)語:
本文主要講述在使用Retrofit和RxJava做網(wǎng)絡(luò)請求庫時,從基礎(chǔ)網(wǎng)絡(luò)配置,通用實體定義,Cookie相關(guān)處理,調(diào)用方式優(yōu)化,服務(wù)器錯誤碼及自定義異常的全局處理,公共請求參數(shù)Header的統(tǒng)一添加,全局取消網(wǎng)絡(luò)請求回調(diào)等項目實踐中容易遇到的問題的一些解決方案。還有其他如添加緩存,接口加密解密等比較常見的場景后續(xù)可以擴展。
因時間關(guān)系文章難免有疏漏,歡迎提出指正,謝謝。同時對RxJava和Retrofit感興趣的童鞋可以參考以下鏈接:
1、Retrofit用法詳解
2、給 Android 開發(fā)者的 RxJava 詳解
3、Okhttp-wiki 之 Interceptors 攔截器