RxJava + Retrofit + okhttp 的实际开发
注:此文章不谈其他用法,谈实际开发中最普遍的用法
分析
实际需求:
返回json格式:
{
"code": 1,
"msg": "",
"time": 1470717690,
"data": T
}
其中data千变万化,但是总体来说是code决定了data的内容,所以在解析response返回值时需要对code进行一系列逻辑处理,这很重要。
以一个最常用的使用场景为例,去服务器取得我的新闻列表,因为我的这个其中涉及了权限验证即token的获取,当然也包含了token状态的过期以及服务器异常导致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'
构建工具类
我的主包: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相关类(一共三个)
因为官方自带的这个处理转化类实在是粗糙,如果data的格式不对,那么会直接抛出JsonSyntaxException,这就很暴力了,我们应该根据code来判断相应的逻辑。举个例子:想获取新闻列表,那么data是数组格式,但是如果token过期,被中间件拦截(可以理解成权限验证),这个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 正常情况下错误状态
- 定义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;
}
}
- 定义返回值类,注意和上面的区别
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.jianshu.com/p/5b8b1062866b
因为你只能对ResponseBody读取一次 , 如果你调用了response.body().string()两次或者response.body().charStream()两次就会出现这个异常, 先调用string()再调用charStream()也不可以。
所以通常的做法是读取一次之后就保存起来,下次就不从ResponseBody里读取。
Rxjava主题逻辑
好了做了这么多的准备工作重要,开始重头戏了,先说一下思路,
判断token:
----1为null:抛出GetTokenException,在retryWhen中用存储的账号网络请求token
----2不为空:flatMap中请求news数据1中:
----1.1账号错误,抛出NeedLoginException,那么直接跳到登入页面
----1.2获得了token,存储起来,并且flatMap中请求news数据1.2中:
----1.2.1如果服务器出现异常,即无法验证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过期,或者刚刚客户端注销等业务判断 需要抛出一个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: 还剩下网络异常处理
}
}
@Override
public void onNext(List<KrNews> krNewses) {
//TODO: 更新ui
}
});
}
附上我测试的结果图