注:此文章不談其他用法涛目,談實際開發(fā)中最普遍的用法
分析
實際需求:
返回json格式:
{
"code": 1,
"msg": "",
"time": 1470717690,
"data": T
}
其中data千變?nèi)f化怠惶,但是總體來說是code決定了data的內(nèi)容扰才,所以在解析response返回值時需要對code進行一系列邏輯處理,這很重要凌停。
以一個最常用的使用場景為例侮东,去服務器取得我的新聞列表圈盔,因為我的這個其中涉及了權(quán)限驗證即token的獲取,當然也包含了token狀態(tài)的過期以及服務器異常導致token無法通過驗證的場景悄雅,基本所有情況都考慮到了驱敲。
下面我以代碼的順序為主來說明,防止扯來扯去宽闲,扯的自己都不知道說道哪里了众眨。
前提:最好了解一點相關知識,關于三者Rxjava容诬、Retrofit娩梨、Okhttp
gradle 引入的一些類庫(三者Rxjava、Retrofit览徒、Okhttp)
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.1.7'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.orhanobut:logger:1.15'
構(gòu)建工具類
我的主包:com.rz.app
public class HttpMethods {
private static OkHttpClient okHttpClient = new OkHttpClient();
private static Converter.Factory gsonConverterFactory = GsonConverterFactory.create();
private static Converter.Factory advancedGsonConverterFactory = com.rz.app.api.convert.GsonConverterFactory.create();
private static CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create();
public static Retrofit getApi() {
Retrofit retrofit = new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("http://api.com/")
.addConverterFactory(advancedGsonConverterFactory)
.addCallAdapterFactory(rxJavaCallAdapterFactory)
.build();
return retrofit;
}
}
重寫gsoncovertfactory相關類(一共三個)
因為官方自帶的這個處理轉(zhuǎn)化類實在是粗糙狈定,如果data的格式不對,那么會直接拋出JsonSyntaxException习蓬,這就很暴力了纽什,我們應該根據(jù)code來判斷相應的邏輯。舉個例子:想獲取新聞列表友雳,那么data是數(shù)組格式稿湿,但是如果token過期,被中間件攔截(可以理解成權(quán)限驗證)押赊,這個data就不返回或者干脆返回空值饺藤,那么會拋出這個異常,無法獲取code不能準確做出相應的邏輯處理流礁。
先和服務端約定好錯誤碼
- 我定義此異常
//code 為-9
public class ErrorException extends RuntimeException{
public ErrorException(String s) {
super(s);
}
}
//code 為-1
public class GetTokenException extends RuntimeException {
}
//code 為0
public class MsgException extends RuntimeException{
public MsgException(String s) {
super(s);
}
}
//code 為-2
public class NeedLoginException extends RuntimeException{
public NeedLoginException(String s) {
super(s);
}
}
解釋一下:
-9 請求錯誤(api的url錯誤)涕俗、服務端異常
-2 賬號有誤,客戶端記錄的賬號不正確,會跳到登入頁面
-1 token過期
0 正常情況下錯誤狀態(tài)
- 定義code的result類,用來作為中間返回值類
public class Result {
private int code;
private String msg;
private int time;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
}
- 定義返回值類高帖,注意和上面的區(qū)別
public class Msg<T> {
private int code;
private String msg;
private int time;
private T data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
- copy源碼,首先copy過來三個文件元镀,其實主要是修改GsonResponseBodyConverter绍填。
@Override
public T convert(ResponseBody value) throws IOException {
String respose = value.string();
Result msg = gson.fromJson(respose, Result.class); // Result即為上面定義的中間返回值類
if (msg.getCode() < 1) {
value.close();
switch (msg.getCode()) {
case -9:
throw new ErrorException(msg.getMsg());
case -2:
throw new NeedLoginException("需要登入");
case -1:
Logger.d("token 過期");
throw new GetTokenException();
case 0:
throw new MsgException(msg.getMsg());
}
throw new ErrorException("未定義錯誤碼");
}
MediaType mediaType = value.contentType();
Charset charset = mediaType != null ? mediaType.charset(UTF_8) : UTF_8;
InputStream inputStream = new ByteArrayInputStream(respose.getBytes());
Reader reader = new InputStreamReader(inputStream,charset);
JsonReader jsonReader = gson.newJsonReader(reader);
try {
return adapter.read(jsonReader);
} finally {
value.close();
}
}
注意這里有個坑,使用不當會拋出java.lang.IllegalStateException栖疑,可以參考這位小伙伴http://www.reibang.com/p/5b8b1062866b
因為你只能對ResponseBody讀取一次 , 如果你調(diào)用了response.body().string()兩次或者response.body().charStream()兩次就會出現(xiàn)這個異常, 先調(diào)用string()再調(diào)用charStream()也不可以讨永。
所以通常的做法是讀取一次之后就保存起來,下次就不從ResponseBody里讀取。
Rxjava主題邏輯
好了做了這么多的準備工作重要遇革,開始重頭戲了卿闹,先說一下思路,
判斷token:
----1為null:拋出GetTokenException萝快,在retryWhen中用存儲的賬號網(wǎng)絡請求token
----2不為空:flatMap中請求news數(shù)據(jù)1中:
----1.1賬號錯誤锻霎,拋出NeedLoginException,那么直接跳到登入頁面
----1.2獲得了token揪漩,存儲起來旋恼,并且flatMap中請求news數(shù)據(jù)1.2中:
----1.2.1如果服務器出現(xiàn)異常,即無法驗證token氢拥,返回-1蚌铜,拋出GetTokenException,會重復獲取token如果不加處理嫩海,那就gg了,所以加個zipWith囚痴,只請求三次叁怪,第四次直接拋出ErrorException("服務器異常"),遠離gg2中
----返回-1深滚,token過期奕谭,那么同1.2.1
retryWhen為中間處理層,subscribe中onError終極處理
假設token我們直接定義為
public class Token extends BaseModel {
private String actoken;
private int time;
public String getActoken() {
return actoken;
}
public void setActoken(String actoken) {
this.actoken = actoken;
}
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
}
接口如下定義痴荐,News為新聞類(就不貼了)
public class ApiService {
public interface GetTokenApi {
@GET("index/gettoken/mobile/{mobile}/password/{password}")
Observable<Msg<Token>> result(@Path("mobile") String mobile,@Path("password") String password);
}
public interface NewsApi {
@GET("mycenter/getNews/token/{token}")
Observable<Msg<List<News>>> result(@Path("token") String token);
}
}
下面開始Rxjava處理主要邏輯
protected Token token=null;
protected Subscription subscription;
//解除訂閱
protected void unsubscribe() {
if (subscription != null && !subscription.isUnsubscribed()) {
subscription.unsubscribe();
}
}
...
public void getNews(){
unsubscribe();
subscription = Observable.just(null)
.flatMap(new Func1<Object,Observable<List<News>>>(){
@Override
public Observable<List<News>> call(Object o) {
if(token == null){
Logger.d("token為null");
return Observable.error(new GetTokenException());
}
Logger.d("使用緩存的token");
// TODO: 本地判斷token是否過期血柳,當然服務器也會二次判斷
return getApi().create(ApiService.NewsApi.class)
.result(token.getActoken())
.map(new Func1<Msg<List<News>>, List<News>>() {
@Override
public List<News> call(Msg<List<News>> listMsg) {
return listMsg.getData();
}
});
}
})
.retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
@Override
public Observable<?> call(Observable<? extends Throwable> observable) {
return observable
.zipWith(Observable.range(1, 4), new Func2<Throwable, Integer, Throwable>() {
@Override
public Throwable call(Throwable throwable, Integer integer) {
if(integer == 4){
throw new ErrorException("服務器異常");
}
return throwable;
}
})
.flatMap(new Func1<Throwable, Observable<?>>() {
@Override
public Observable<?> call(final Throwable throwable) {
if (throwable instanceof GetTokenException) {
//TODO 獲取存儲的賬戶信息,用來獲取token
//如果沒有生兆,即首次登入难捌,或者token過期,或者剛剛客戶端注銷等業(yè)務判斷 需要拋出一個NeedLoginExcption
// 這里假設有記錄
boolean firstLogin = false;
boolean tokenExpired = false;
boolean logoff = false;
if(firstLogin || tokenExpired || logoff){
return Observable.error(new NeedLoginException("需要登入"));
}
return getApi()
.create(ApiService.GetTokenApi.class)
.result("12345678910", "123456")
.map(new Func1<Msg<Token>, Token>() {
@Override
public Token call(Msg<Token> msg) {
return msg.getData();
}
})
.doOnNext(new Action1<Token>() {
@Override
public void call(Token t) {
Logger.d("存儲token");
//TODO: 存入緩存等
token = t;
}
});
}
return Observable.error(throwable);
}
});
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<List<News>>() {
@Override
public void onCompleted() {
//TODO: 完成邏輯
}
@Override
public void onError(Throwable throwable) {
if(throwable instanceof NeedLoginException){
//TODO: 跳到登入頁面
}else if(throwable instanceof ErrorException){
//TODO: 提示 throwable.getMessage()
}else if(throwable instanceof MsgException) {
//TODO: 提示 throwable.getMessage()
}else {
Logger.d(throwable.getClass());
//TODO: 還剩下網(wǎng)絡異常處理
}
}
@Override
public void onNext(List<KrNews> krNewses) {
//TODO: 更新ui
}
});
}
附上我測試的結(jié)果圖