转自:http://www.jianshu.com/p/2b0aeb6b6b61
从年前一两个月开始,就开始慢慢接触RxJava+Retrofit,针对以往开发中遇到的情况,慢慢写了一个框架Demo。文章不在进行入门介绍,需要了解的同学,可以查看笔者总结的文章RxJava、Retrofit
分割Response
一般来说,网络请求结果包括以下信息:
{
"message": "操作成功",
"code": "1",
"object": {} }
我们可以定义一个对象Response,其中泛型T来表示object,可能是数组,也可能是对象。code为1(或者其他值,和后台商议)表示接口调用成功,如:登录成功,注册成功等;code为其他值,则表示失败,如登录失败等,此时message便返回对应的错误信息,如密码错误等。
如果返回结果为Response,则每次网络请求都要判断接口是否调用成功,比较麻烦,我们希望的是:如果接口调用成功,返回泛型T,即object;如果调用失败,则返回code、message信息。因此,需要对返回结果进行分割处理。
分割操作代码如下:
/** * 对网络接口返回的Response进行分割操作 * * @param response * @param <T> * @return */
public <T> Observable<T> flatResponse(final Response<T> response) {
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
if (response.isSuccess()) {
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(response.object);
}
} else {
if (!subscriber.isUnsubscribed()) {
subscriber.onError(new APIException(response.code, response.message));
}
return;
}
if (!subscriber.isUnsubscribed()) {
subscriber.onCompleted();
}
}
});
}
其中response.isSuccess()的代码如下:
public boolean isSuccess() {
return code.equals(Constant.OK);
}
通过以上代码,便可实现分割操作,这样每次返回结果都不用通过code来判断是否成功。
有些时候,为了方便调试,我们需要将网络请求的地址和参数log出来。由于Retrofit是基于OKHttp的,所以我们需要通过Interceptors来拦截OKHttp来log所需信息。
关于Interceptors,不再多说,直接附上代码。代码来自HttpLoggingInterceptor ,做了简化。
package com.sunflower.rxandroiddemo.utils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.internal.Platform;
import okio.Buffer;
/** * Created by Sunflower on 2016/1/12. */
public class HttpLoggingInterceptor implements Interceptor {
private static final Charset UTF8 = Charset.forName("UTF-8");
public enum Level {
/** * No logs. */
NONE,
/** * Logs request and response lines. * <p/> * Example: * <pre>{@code * --> POST /greeting HTTP/1.1 (3-byte body) * <p/> * <-- HTTP/1.1 200 OK (22ms, 6-byte body) * }</pre> */
BASIC,
/** * Logs request and response lines and their respective headers. * <p/> * Example: * <pre>{@code * --> POST /greeting HTTP/1.1 * Host: example.com * Content-Type: plain/text * Content-Length: 3 * --> END POST * <p/> * <-- HTTP/1.1 200 OK (22ms) * Content-Type: plain/text * Content-Length: 6 * <-- END HTTP * }</pre> */
HEADERS,
/** * Logs request and response lines and their respective headers and bodies (if present). * <p/> * Example: * <pre>{@code * --> POST /greeting HTTP/1.1 * Host: example.com * Content-Type: plain/text * Content-Length: 3 * <p/> * Hi? * --> END GET * <p/> * <-- HTTP/1.1 200 OK (22ms) * Content-Type: plain/text * Content-Length: 6 * <p/> * Hello! * <-- END HTTP * }</pre> */
BODY
}
public interface Logger {
void log(String message);
/** * A {@link Logger} defaults output appropriate for the current platform. */
Logger DEFAULT = new Logger() {
@Override
public void log(String message) {
Platform.get().log(message);
}
};
}
public HttpLoggingInterceptor() {
this(Logger.DEFAULT);
}
public HttpLoggingInterceptor(Logger logger) {
this.logger = logger;
}
private final Logger logger;
private volatile Level level = Level.BODY;
/** * Change the level at which this interceptor logs. */
public HttpLoggingInterceptor setLevel(Level level) {
if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
this.level = level;
return this;
}
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Level level = this.level;
Request request = chain.request();
if (level == Level.NONE) {
return chain.proceed(request);
}
boolean logBody = level == Level.BODY;
boolean logHeaders = logBody || level == Level.HEADERS;
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
String requestStartMessage = request.method() + ' ' + request.url();
if (!logHeaders && hasRequestBody) {
requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
}
logger.log(requestStartMessage);
if (logHeaders) {
if (!logBody || !hasRequestBody) {
logger.log("--> END " + request.method());
} else if (bodyEncoded(request.headers())) {
logger.log("--> END " + request.method() + " (encoded body omitted)");
} else if (request.body() instanceof MultipartBody) {
//如果是MultipartBody,会log出一大推乱码的东东
} else {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
contentType.charset(UTF8);
}
logger.log(buffer.readString(charset));
// logger.log(request.method() + " (" + requestBody.contentLength() + "-byte body)");
}
}
long startNs = System.nanoTime();
Response response = chain.proceed(request);
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
logger.log(response.code() + ' ' + response.message() + " (" + tookMs + "ms" + ')');
return response;
}
private boolean bodyEncoded(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
}
private static String protocol(Protocol protocol) {
return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
}
}
这样在初始化Retrofit时,我们可以通过以下代码来log请求地址+参数
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Log.i("RxJava", message);
}
});
OkHttpClient client = new OkHttpClient.Builder()
//log请求参数
.addInterceptor(interceptor)
.build();
结果如下:
对于一个APP来说,我们需要建立一个或者多个接口(我们先分析一个接口的情况,下文用APIService来替代),里面是相应的网络请求,然而不可能每次请求都初始化一个Retrofit对象,进而获得APIService对象,传入对应参数,进行网络请求,处理返回结果。
所以,首先可以新建RetrofitUtil类,用于初始化操作,网络结果分割操作等等;然后新建ApiWrapper封装类(继承自RetrofitUtil)。新建ApiWrapper封装类有什么好处呢?用代码来说明吧!
比说在APIService中有这样一个网络请求方法:
@FormUrlEncoded @POST("api/common/msg.json") Observable<Response<String>> getSmsCode(@Field("mobile") String mobile, @Field("appType") String appType);
该方法是用来获取短信验证码的,需要传入两个参数:手机号、app类型(医生端or孕妇端)
由于返回结果为验证码,即object字段为String类型,所以返回结果是Response
通过ApiWrapper封装后,代码如下:
public Observable<String> getSmsCode(String mobile) {
return getService().getSmsCode(mobile, "GRAVIDA")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<Response<String>, Observable<String>>() {
@Override
public Observable<String> call(Response<String> stringResponse) {
return flatResponse(stringResponse);
}
});
}
其中getService()为父类RetrofitUtil中获取APIService对象的方法。
这样的话,在对应Activity中调用起来就很方便了
ApiWrapper wrapper = new ApiWrapper();
wrapper.getSmsCode(mobile)
.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(String s) {
Log.i(TAG, "call " + s);
}
});
通过以上代码,我们可以发现封装类有以下好处:
public Observable<String> getSmsCode(String mobile) {
return getService().getSmsCode(mobile, "GRAVIDA")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<Response<String>, Observable<String>>() {
@Override
public Observable<String> call(Response<String> stringResponse) {
return flatResponse(stringResponse);
}
});
}
从这个方法中,我们可以清楚地看到数据是如何在一系列操作符之间进行转换的。但是以后每个网络请求都将进行这样的重复操作。
如何将一组操作符重用于多个数据流中呢?例如,因为希望在工作线程中处理数据,在主线程中处理结果,然后分割网络请求结果。所以我会频繁使用subscribeOn()、observeOn()、flatMap()。如果我能够通过重用的方式,将这种逻辑运用到我所有的数据流中,将是一件多么棒的事。
RxJava提供了一种解决方案:Transformer(有转换器意思),一般情况下可以通过使用操作符Observable.compose()来实现。
Transformer实际上就是一个Func1
/** * * @param <T> * @return */
protected <T> Observable.Transformer<Response<T>, T> applySchedulers() {
return new Observable.Transformer<Response<T>, T>() {
@Override
public Observable<T> call(Observable<Response<T>> responseObservable) {
return responseObservable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<Response<T>, Observable<T>>() {
@Override
public Observable<T> call(Response<T> tResponse) {
return flatResponse(tResponse);
}
})
;
}
};
}
恩,没错,这一部分内容参考了注释内的链接,大家可以去看下这篇帖子。
通过上面的方法,我们将Observable
public Observable<String> getSmsCode(String mobile) {
return getService().getSmsCode(mobile, "GRAVIDA")
.compose(this.<String>applySchedulers());
}
由于要经常调用applySchedulers()方法,可以考虑创造一个实例化Transformer,节省不必要的实例化对象。代码如下:
final Observable.Transformer transformer = new Observable.Transformer() {
@Override
public Object call(Object observable) {
return ((Observable) observable).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1() {
@Override
public Object call(Object response) {
return flatResponse((Response<Object>)response);
}
})
;
}
};
protected <T> Observable.Transformer<Response<T>, T> applySchedulers() {
return (Observable.Transformer<Response<T>, T>) transformer;
}
Note
.flatMap(new Func1() {
@Override
public Object call(Object response) {
return flatResponse((Response)response);
}
})
flatResponse()进行类型强转的话,应该没问题吧?笔者暂时不确定,但目前也没发现什么问题。
封装Subscriber
在Activity中我们调用getSmsCode()代码如下:
ApiWrapper wrapper = new ApiWrapper();
showLoadingDialog();
wrapper.getSmsCode(mobile)
.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(String s) {
Log.i(TAG, "call " + s);
}
});
其实,对于大部分请求来说,我们只需处理onNext() 方法,默认在onCompleted()方法中hideLoadingDialog();在onError()方法中Toast对应的错误信息。
所以我们可以进一步封装Subscriber,代码如下:
/** * 创建观察者 * * @param onNext * @param <T> * @return */
protected <T> Subscriber newSubscriber(final Action1<? super T> onNext) {
return new Subscriber<T>() {
@Override
public void onCompleted() {
hideLoadingDialog();
}
@Override
public void onError(Throwable e) {
if (e instanceof RetrofitUtil.APIException) {
RetrofitUtil.APIException exception = (RetrofitUtil.APIException) e;
showToast(exception.message);
} else if (e instanceof SocketTimeoutException) {
showToast(e.getMessage());
} else if (e instanceof ConnectException) {
showToast(e.getMessage());
}
Log.e(TAG, String.valueOf(e.getMessage()));
//e.printStackTrace();
hideLoadingDialog();
}
@Override
public void onNext(T t) {
if (!mCompositeSubscription.isUnsubscribed()) {
onNext.call(t);
}
}
};
}
在onError()方法中,可以根据Throwable e的类型进行对应处理,其中APIException是我们自定义的异常,SocketTimeoutException、ConnectException则是OKHttp返回的异常。
在onCompleted()和onError()中,我们都需要hideLoadingDialog()。
在subscribe()之后, Observable会持有 Subscriber的引用,这个引用如果不能及时被释放,将有内存泄露的风险。所以最好保持一个原则:要在不再使用的时候尽快在合适的地方(例如 onPause()、onStop()等方法中)调用 unsubscribe()来解除引用关系,以避免内存泄露的发生。
我们可以在BaseActivity中声明一个对象
/** * 使用CompositeSubscription来持有所有的Subscriptions */
protected CompositeSubscription mCompositeSubscription;
在onCreate()方法中初始化:
mCompositeSubscription = new CompositeSubscription();
在onDestroy()中unsubscribe()接触引用关系:
@Override
protected void onDestroy() {
super.onDestroy();
//一旦调用了 CompositeSubscription.unsubscribe(),这个CompositeSubscription对象就不可用了,
// 如果还想使用CompositeSubscription,就必须在创建一个新的对象了。
mCompositeSubscription.unsubscribe();
}
在Activity中调用网络请求时:
Subscription subscription = wrapper.getSmsCode2("15813351726")
.subscribe(newSubscriber(new Action1<String>() {
@Override
public void call(String s) {
Log.i(TAG, "call " + s);
}
}));
mCompositeSubscription.add(subscription);
所以在newSubscriber()中的onNext()方法中,我们需要事先判断mCompositeSubscription是否已经解除了引用。
代码地址在RxAndroidDemo https://github.com/sunflower-zyb/RxAndroidDemo
下面主要说明在日常工作中常用到请求。
假设这样一个需求,需要取消收藏多篇文章,以数组的形式传递文章id,代码如下:
@FormUrlEncoded @POST("api/gravida/article/unfavourite.json") Observable<Response<Object>> cancelFavorite(@Field("id") String id, @Field("articleId") List<Long> articleId);
这样,只需传递List articleId即可
一般在更新个人资料时,需要上传头像文件
@Multipart @POST("api/gravida/personal/update.json") Observable<Response<PersonalInfo>> updatePersonalInfo(@PartMap Map<String, RequestBody> params);
使用postman查看时,
会发现编码方式为multipart/form-data,另外需要注意头像(文件)的参数是:
Content-Disposition: form-data; name="avatar"; filename="Chrysanthemum.jpg"
Content-Type: image/jpeg
封装后的代码是:
/** * 上传单个文件 * * @param path 文件路径 * @return */
public Observable<PersonalInfo> updatePersonalInfo(String path) {
File file = new File(path);
RequestBody id = RequestBody.create(MediaType.parse("text/plain"), "139");
//直接传递文件
//RequestBody avatar = RequestBody.create(MediaType.parse("image/*"), file);
//传递byte[]
Bitmap bitmap = ClippingPicture.decodeBitmapSd(path);
RequestBody avatar = RequestBody.create(MediaType.parse("image/*"), ClippingPicture.bitmapToBytes(bitmap));
Map<String, RequestBody> params = new HashMap<>();
params.put("id", id);
params.put("avatar\"; filename=\"" + file.getName() + "", avatar);
return getService().updatePersonalInfo(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<Response<PersonalInfo>, Observable<PersonalInfo>>() {
@Override
public Observable<PersonalInfo> call(Response<PersonalInfo> personalInfoResponse) {
return flatResponse(personalInfoResponse);
}
});
}
通过RequestBody.create()创建RequestBody对象。对于文件,可以使用File,亦可以使用byte[]。
请注意Map
如果需要同时上传多个文件,代码如下:
/** * 同时传递多个文件 * * @param orderId 订单id * @param productId 产品id * @param content 评论内容 * @param paths 评论的图片路径 * @return */
public Observable<Object> commentProduct(long orderId, long productId, String content, List<String> paths) {
RequestBody id = RequestBody.create(MediaType.parse("text/plain"), "166");
RequestBody orderIdBody = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(orderId));
RequestBody productIdBody = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(productId));
RequestBody contentBody = RequestBody.create(MediaType.parse("text/plain"), content);
Map<String, RequestBody> params = new HashMap<>();
params.put("id", id);
params.put("orderId", orderIdBody);
params.put("productId", productIdBody);
params.put("content", contentBody);
for (String image : paths) {
File file = new File(image);
RequestBody images = RequestBody.create(MediaType.parse("image/*"), file);
params.put("images\"; filename=\"" + file.getName() + "", images);
}
return getService().commentProduct(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<Response<Object>, Observable<?>>() {
@Override
public Observable<?> call(Response<Object> objectResponse) {
return flatResponse(objectResponse);
}
})
;
}
for (String image : paths) {
File file = new File(image);
RequestBody images = RequestBody.create(MediaType.parse("image/*"), file);
//key值中为images
params.put("images\"; filename=\"" + file.getName() + "", images);
}
通过循环来添加文件,其中key值和postman中的参数一致。
在开发过程中,同一个页面的数据是由多个接口共同组成的。所有接口调用完毕后,才可以显示页面数据,结束loading框。
假如我们要在首页同时请求以下三个接口
/** * 检查版本 * * @param version * @param type * @param device * @return */
@FormUrlEncoded
@POST("api/common/version.json")
Observable<Response<VersionDto>> checkVersion(@Field("version") String version,
@Field("type") String type,
@Field("device") String device);
/** * 获取个人信息 * * @param id * @return */
@FormUrlEncoded
@POST("api/gravida/personal/info.json")
Observable<Response<PersonalInfo>> getPersonalInfo(@Field("id") String id);
/** * 获取个人配置信息 * * @param id * @return */
@FormUrlEncoded
@POST("api/gravida/personal/configs.json")
Observable<Response<PersonalConfigs>> getPersonalConfigs(@Field("id") String id);
那么在首页时,我们可以这么做:
//将多个接口的返回结果结合成一个对象
Observable.zip(wrapper.checkVersion(), wrapper.getPersonalInfo(), wrapper.getPersonalConfigs(),
new Func3<VersionDto, PersonalInfo, PersonalConfigs, HomeRequest>() {
@Override
public HomeRequest call(VersionDto versionDto, PersonalInfo personalInfo, PersonalConfigs personalConfigs) {
HomeRequest request = new HomeRequest();
request.setVersionDto(versionDto);
request.setPersonalInfo(personalInfo);
request.setPersonalConfigs(personalConfigs);
return request;
}
})
.subscribe(newSubscriber(new Action1<HomeRequest>() {
@Override
public void call(HomeRequest request) {
Log.i(TAG, "versionDto--" + request.getVersionDto().toString());
Log.i(TAG, "personalInfo--" + request.getPersonalInfo().toString());
Log.i(TAG, "PersonalConfigs--" + request.getPersonalConfigs().toString());
}
}))
;
通过RxJava的zip()操作符,我们可以将网络请求组合起来,并用Func3(因为是将3个操作组合在一起了)将3个接口的返回结果组合成HomeRequest
/** * Represents a function with three arguments. */
public interface Func3<T1, T2, T3, R> extends Function {
R call(T1 t1, T2 t2, T3 t3);
}
有时候,我们需要用A接口的请求结果来请求B接口。
如:需要首先获取帖子分类列表,根据帖子分类id,进而获取该分类的帖子列表。
接口如下:
/** * 获取帖子分类列表 * * @return */
@POST("api/gravida/article/categories.json")
Observable<Response<List<ArticleCategory>>> getArticleCategory();
/** * 根据分类获取帖子列表 * * @param id 分类id * @param pageNumber * @param pageSize * @return */
@FormUrlEncoded
@POST("api/gravida/article/list.json")
Observable<Response<List<ArticleListDTO>>> getArticleList(@Field("id") long id,
@Field("pageNumber") int pageNumber,
@Field("pageSize") int pageSize);
在相应页面,代码如下:
wrapper.getArticleCategory()
//可以在doOnNext处理数据
.doOnNext(new Action1<List<ArticleCategory>>() {
@Override
public void call(List<ArticleCategory> articleCategories) {
categoryId = articleCategories.get(0).getId();
}
})
.flatMap(new Func1<List<ArticleCategory>, Observable<List<ArticleListDTO>>>() {
@Override
public Observable<List<ArticleListDTO>> call(List<ArticleCategory> articleCategories) {
return wrapper.getArticleList(categoryId, 1);
}
})
.subscribe(newSubscriber(new Action1<List<ArticleListDTO>>() {
@Override
public void call(List<ArticleListDTO> articleList) {
for (ArticleListDTO article : articleList) {
Log.i(TAG, article.getId() + " " + article.getTitle() + " " + article.getIntro());
}
}
}));
我们可以在doOnNext()中对数据进行处理,如保存categoryId帖子id,通过一个flatMap()操作将List转化为Observable
有些接口因网络异常或者超时时,需要多次请求。代码如下:
wrapper.getArticleCategory()
//可以在doOnNext处理数据
.doOnNext(new Action1<List<ArticleCategory>>() {
@Override
public void call(List<ArticleCategory> articleCategories) {
categoryId = articleCategories.get(0).getId();
}
})
//设置请求次数
.retry(new Func2<Integer, Throwable, Boolean>() {
@Override
public Boolean call(Integer integer, Throwable throwable) {
Log.e(TAG, "call " + integer);
if (throwable instanceof SocketTimeoutException && integer < 2)
return true;
else
return false;
}
})
.subscribe(newSubscriber(new Action1<List<ArticleCategory>>() {
@Override
public void call(List<ArticleCategory> articleCategories) {
}
}));
如果是SocketTimeoutException连接超时,或者integer请求次数小于2,则返回true,即需要retry()。
RxJava还有很多操作符,大家可以根据自己的需求来使用。