RxJava +Retrofit2 + okhttp3
好记性不如烂笔头,整理一下之前集成RRO框架时遇到的问题,和集成的过程,希望也能给有找问题的人一些帮助。
这个一套的集成,网上有大把的例子了,所以这里只是做一个代码实现上的介绍以及过程中需要注意的地方。
1. 首先引入依赖
dependencies {
//OkHttp
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.3.0'//导入retrofit
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'//转换器
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'//Rxjava支持
//这里我直接引入了Rxbinding,里面已经包含了Rxjava2
implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2'
}
2. 创建RetrofitServiceManager Retrofit2管理类
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import retrofit2.Converter;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitServiceManager {
private static final int DEFAULT_CONNECT_TIME_OUT = 15;
private static final int DEFAULT_ACTION_TIME_OUT = 15;
//这里先首先声明
private Retrofit retrofit;
public RetrofitServiceManager(String baseUrl, List<Interceptor> interceptors, List<Converter.Factory> factories) {
//通过构建器 创建OKHttp客户端
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//链接超时时间
builder.connectTimeout(DEFAULT_CONNECT_TIME_OUT, TimeUnit.SECONDS);
//写入超时时间
builder.writeTimeout(DEFAULT_ACTION_TIME_OUT, TimeUnit.SECONDS);
//读取超时时间
builder.readTimeout(DEFAULT_ACTION_TIME_OUT, TimeUnit.SECONDS);
builder.retryOnConnectionFailure(true);
if (null != interceptors) {//添加拦截器
for (Interceptor interceptor : interceptors) {
builder.addInterceptor(interceptor);
}
}
Retrofit.Builder myRetrofit = new Retrofit.Builder().client(builder.build());
if (null != factories && factories.size() > 0) {//添加转换器
for (Converter.Factory factory : factories) {
myRetrofit.addConverterFactory(factory);
}
}
//这里的NullOnEmptyConverterFactory,因为我所负责项目接口可能会什么都不返回
//我觉得很奇葩,只能这里先做拦截处理了,避免出现转换异常
//如果你不需要,也可以去掉。
myRetrofit.addConverterFactory(new NullOnEmptyConverterFactory())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(baseUrl);
retrofit = myRetrofit.build();
}
private static RetrofitServiceManager serviceManager;
//单例
public static RetrofitServiceManager getInstance(String baseUrl) {
return getInstance(baseUrl, null, null);
}
//单例
public static RetrofitServiceManager getInstance(String baseUrl, List<Interceptor> interceptors) {
return getInstance(baseUrl, interceptors, null);
}
public static RetrofitServiceManager getInstance(String baseUrl, List<Interceptor> interceptors, List<Converter.Factory> factories) {
if (serviceManager == null) {
synchronized (RetrofitServiceManager.class) {
if (serviceManager == null) {
serviceManager = new RetrofitServiceManager(baseUrl, interceptors, factories);
}
}
}
return serviceManager;
}
public <T> T create(Class<T> service) {
return retrofit.create(service);
}
}
3. 首先创建BaseTask,主要目的是将一些重复的代码抽出来,交由父类去实现,子类只关心访问哪个接口,如何访问…
import android.content.Context;
import org.json.JSONObject;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import okhttp3.MediaType;
import okhttp3.RequestBody;
public class BaseTask {
private String TAG = "BaseTask";
//指定观察者和被观察者的线程
protected <T> Observable<T> useThread( Observable<T> observable) {
return observable
//指定被观察者执行的线程
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
//指定观察者执行的线程(我们这里通常是这样的操作,当然我们还可以在Task中自行切换线程)
.observeOn(AndroidSchedulers.mainThread());
}
//将键值对的参数转换成JsonBody
public RequestBody getJSonBody(Map<String, Object> key) {
String json = new JSONObject(key).toString();
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
}
/**
* 将参数封装成requestBody
* @param param 参数
* @return RequestBody
*/
public RequestBody convertToRequestBody(String param) {
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), param);
return requestBody;
}
/**
* 将File封装成RequestBody
*
* @param param 为文件类型
* @return 返回一个RequestBody
*/
public RequestBody convertToRequestBody(File param) {
RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), param);
return requestBody;
}
/**
*同上面的方法大同小异,不过这里我们是处理包含
*File类型参数的键值对转换成 Map<String, RequestBody>
*用于文件上传一类的操作
**/
public Map<String, RequestBody> convertToPartMap(Map<String, Object> map) {
Map<String, RequestBody> requestBodyMap = new HashMap<>();
for (String key : map.keySet()) {
Object value = map.get(key);
if (value instanceof File) {//如果是File我们这里进行转换操作
File file = (File) value;
//这里需要格式拼接一下
requestBodyMap.put(key + "\";filename=\"" + file.getName(), convertToRequestBody(file));
} else {
requestBodyMap.put(key, convertToRequestBody(String.valueOf(value)));
}
}
return requestBodyMap;
}
4. 创建我们自己的Task
import android.content.Context;
//这里的引用我就只留公共的了
import java.util.HashMap;
import java.util.Map;
import io.reactivex.Observable;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
public class LoginTask extends BaseTask {
private LoginService loginService;
private Context context;
public LoginTask(Context context) {
this.context = context;
//注意我们在这里使用的是单例,但是有些情况下,一个应用可能不止一个接口,
//这里baseURl已经在第一次创建时确定了。那么后面的baseurl就不会更改了。
//怎么办呢?
//那我们这里可以直接 new RetrofitServiceManager(x,x,x) 来获取管理类的对象。
this.loginService = RetrofitServiceManager.getInstance(HttpConfig.HOST_STRING).create(LoginService.class);
}
public Observable<LoginBean> login(Map<String, Object> map) {
//注:这里我们进行的操作都必须放在useThread方法中,在BaseTask这里我们进行了线程的切换,否则会抛出异常,原因是因为在主线程中进行网络操作
return useThread(context, loginService.login(map)
//这里flatMap函数对Observable进行了对象的转换,在最终的回调,我们就只用关心自己的那个数据了。
//这里使用java8的语法糖lambda表达式,不了解lambda表达式的可以百度一下就理解stringBaseBean 是什么对象了。
.flatMap(stringBaseBean -> {
if (!stringBaseBean.isCode()) {
//如果这里后台Json返回了非正常状态码,省事一点我们直接抛出自定义异常
//将异常流程交由onError()去处理
//BusinessException 自定义业务异常(英语渣,大噶能看懂我表达的意思就好了)
throw new BusinessException(stringBaseBean.getMsg());
}
//如果数据是正常的,那么我们就只需要把我们需要的数据,
//利用Observable.just()立即发射回去就好了
return Observable.just(stringBaseBean.getData());
}));
}
//自定义接口
public interface LoginService {
@FormUrlEncoded //指定编码格式
@POST(HttpConfig.LOGIN)//指定接口,个人比较喜欢统一写在一个类中管理
Observable<BaseBean<LoginBean>> login(@FieldMap Map<String, Object> map);
}
}
LoginService 这里 Retrofit2的注解我就不多说了,网上很多,我自己也不是记得很清楚,一会儿我会在后面贴一下之前遇到的坑。
这里是ResultException,这里统一处理了一下异常,封装了ResultException
import androidx.annotation.Nullable;
import com.futurekang.buildtools.net.retrofit.exception.BusinessException;
import com.google.gson.JsonSyntaxException;
import java.net.ConnectException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import retrofit2.HttpException;
public class ResultException extends Exception {
//网络异常
public final static int NETWORK_EXCEPTION = 0X001;
//系统错误,(指本地客户端解析错误一类的问题)
public final static int SYSTEM_EXCEPTION = 0X002;
//业务流程异常(自定义异常)
public final static int BUSINESS_EXCEPTION = 0X003;
//未知的其他异常
public final static int OTHER_EXCEPTION = 0X004;
private int exceptionType;
private String msg;
private Throwable cause;
private ResultException(String message) {
super(message);
}
private ResultException(String message, Throwable cause) {
super(message, cause);
}
public ResultException(Throwable cause) {
if (cause instanceof UnknownHostException ||
cause instanceof HttpException ||
cause instanceof SocketTimeoutException ||
cause instanceof SocketException) {
exceptionType = NETWORK_EXCEPTION;
if (cause instanceof ConnectException) {
msg = "暂时无法连接到服务器,请稍候再试!";
} else {
msg = "网络连接超时,请检查您的网络状态!";
}
} else if (
cause instanceof IllegalArgumentException ||
cause instanceof JsonSyntaxException) {
exceptionType = SYSTEM_EXCEPTION;
msg = cause.getMessage();//也可以直接写 msg = "解析异常"
} else if (cause instanceof BusinessException) {//业务异常
exceptionType = BUSINESS_EXCEPTION;
msg = cause.getMessage();
} else {//其他异常
exceptionType = OTHER_EXCEPTION;
msg = cause.getMessage();
}
this.cause = cause;
}
public int getExceptionType() {
return exceptionType;
}
public void setExceptionType(int exceptionType) {
this.exceptionType = exceptionType;
}
@Nullable
@Override
public String getMessage() {
return msg;
}
@Nullable
@Override
public synchronized Throwable getCause() {
return cause;
}
}
如果你们公司的后台接口格式较为规范一点的,你的工作量能大大减小~,这里是我项目中常见的Json格式代码 这里用到了泛型
BaseBean代码
public class BaseBean<T> {
private boolean code;
private String msg;
private T data;
public boolean isCode() {
return code;
}
public void setCode(boolean code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
这里不得不说一下,Rxjava配合lambda表达式真的很好用,不过不熟悉的人还是不太习惯的。
这里重贴一下之前的那段代码,照顾一下还没看习惯的人。
1.一般写法
public Observable<LoginBean> login(Map<String, Object> map) {
return useThread(context, loginService.login(map)
.flatMap(new Function<BaseBean<LoginBean>, ObservableSource<? extends LoginBean>>() {
@Override
public ObservableSource<? extends LoginBean> apply(BaseBean<LoginBean> loginBeanBaseBean) throws Exception {
if (!loginBeanBaseBean.isCode()) {
throw new BusinessException(loginBeanBaseBean.getMsg());
}
return Observable.just(loginBeanBaseBean.getData());
}
}));
}
2.语法糖
public Observable<LoginBean> login(Map<String, Object> map) {
return useThread(context, loginService.login(map)
.flatMap(stringBaseBean -> {
if (!stringBaseBean.isCode()) {
throw new BusinessException(stringBaseBean.getMsg());
}
return Observable.just(stringBaseBean.getData());
}));
}
5. 继承DisposableObserver 实现我们自己的NetworkObserver,
在NetworkObserver中,主要做的就是异常的处理及封装,还有进度条的控制
最后调用的时候只关心具体的数据和几种统一处理后的异常。
import java.io.IOException;
import java.util.Objects;
import io.reactivex.observers.DisposableObserver;
import retrofit2.HttpException;
/**
* @author Futurekang
* @createdate 2019/10/10 9:36
* @description 定制化的网络请求Observer
* 控制加载动画,并拦截了网络和业务异常流程,
* 使用时只关心结果和异常流程
*/
@MainThread
public abstract class NetworkObserver<T> extends DisposableObserver<T> {
private Context context;//上下文对象
private boolean showProgress;//是否显示加载动画(默认不显示)
private Object requestCode = -1;//请求码
public NetworkObserver(Context context) {
this.context = context;
}
public NetworkObserver(Context context, boolean showProgress) {
this.context = context;
this.showProgress = showProgress;
}
public NetworkObserver(Context context, Object requestCode) {
this.context = context;
this.requestCode = requestCode;
}
public NetworkObserver(Context context, boolean showProgress, Object requestCode) {
this.context = context;
this.showProgress = showProgress;
this.requestCode = requestCode;
}
@Override//网络请求开始时(处于订阅时的线程)
protected void onStart() {
super.onStart();
//网络状态的判断
if (!NetWorkExceptionUtil.isNetworkAvailable(context)) {
ToastUtils.ShowToast(context, context.getString(R.string.network_unavailable));
if (!isDisposed()) {//当网络请求不可用时主动取消订阅
dispose();
} //网络是否可用
} else if (!NetworkTools.turnOnTheNetwork(context)) {
ToastUtils.ShowToast(context, context.getString(R.string.network_turn));
if (!isDisposed()) {//当网络请求不可用时主动取消订阅
dispose();
}
} else {//网络可用显示进度条
showProgress();
}
}
@Override
public void onNext(T t) {
hideProgress();//获取到数据后,我们隐藏进度条
onSuccess(t);
}
@Override /**重点 异常处理**/
public void onError(Throwable e) {
hideProgress();//
//这里我们利用ResultException ,将可能出现的异常进行了拦截转换成我们统一的异常
ResultException resultException = new ResultException(e);
//这一段由你的业务确定是否需要,
//这里主要是判断非业务异常直接弹窗提示原因
if (resultException.getExceptionType() != ResultException.BUSINESS_EXCEPTION) {
ToastUtils.ShowToast(context, resultException.getMessage());
}
//回调
onError(resultException);
//打印具体网络错误的具体信息
if (e instanceof HttpException) {
HttpException httpException = (HttpException) e;
try {
String error = Objects.requireNonNull(httpException.response().errorBody()).string();
String TAG = "NetworkObserver";
Log.d(TAG, "onError: " + error);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
@Override
public void onComplete() {
}
private BaseDialog progressDialog;
@MainThread
private void showProgress() {
if (!showProgress) {
return;
}
//BaseDialog是我自封装的,这里是我的进度条,用到了LottieAnimationView ,很好用的动画view
if (progressDialog == null) {
progressDialog = new BaseDialog(context, R.layout.view_progress_anim) {
@Override
protected void setChildView(View v) {
LottieAnimationView lottieAnimation = v.findViewById(R.id.lv_animation_view);
lottieAnimation.setAnimation(context.getString(R.string.loading_animator_197));
lottieAnimation.playAnimation();
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(1);
valueAnimator.addUpdateListener(valueAnimator1 -> lottieAnimation.setProgress((Float) valueAnimator1.getAnimatedValue()));
valueAnimator.start();
alterDialog.setOnCancelListener(dialogInterface -> {
lottieAnimation.cancelAnimation();
//取消订阅
if (!NetworkObserver.this.isDisposed()) {
dispose();
}
});
}
};
}
progressDialog.show();
}
@MainThread
protected void hideProgress() {
if (progressDialog != null) {
progressDialog.dismiss();
}
}
public abstract void onSuccess(T data);
public abstract void onError(ResultException message);
6. 使用
Activity中这样使用
伪代码:
Map<String, Object> params = new HashMap();
params.put("username", username);
params.put("password", password);
Disposable disposable = loginTask.login(params)
.subscribeWith(new NetworkObserver<LoginBean>(this) {
@Override
public void onSuccess(LoginBean loginBean) {
......
}
@Override
public void onError(ResultException e) {
......
}
});
addDisposable(disposable);
这样的网络请求是不是很简洁明了啊。
还有就是使用这个框架时的遇到的坑:
1. Retrofit2动态设置URL遇到的问题
@FormUrlEncoded
@POST("user/{url}")
Observable<BaseBean<String>> login(@Path("url") String url);
上面这种写法是url中只需要替换一个部分的情况,但是有时候我们需要整个POST中的url都替换了,比如说这样
@FormUrlEncoded
@POST("{url}")
Observable<BaseBean<String>> login(@Path("url") String url);
但是这样写,我们测试就会发现请求时出错了。为什么会这样呢?我通过测试发现最后替换的上面的url中的斜杠 “/” 都被转义成了“%2F”,为此我专门写了一个拦截器
public class URLInterceptor implements Interceptor {
private String TAG = "URLInterceptor";
@Override
public Response intercept(Chain chain) throws IOException {
Request oldRequest = chain.request();
//构建新的请求,代替原来的请求
Request.Builder requestBuilder = oldRequest.newBuilder();
requestBuilder.method(oldRequest.method(), oldRequest.body());
HttpUrl.Builder authorizedUrlBuilder = oldRequest.url().newBuilder();
HttpUrl oldHttpUrl = authorizedUrlBuilder.build();
URL orgUrl = oldHttpUrl.url();
String url = orgUrl.getProtocol() +
"://" + orgUrl.getAuthority() +
orgUrl.getPath().replace("%2F", "/");
HttpUrl newHttpUrl = HttpUrl.parse(url);
requestBuilder.url(newHttpUrl);
// 新的请求
Request newRequest = requestBuilder.build();
return chain.proceed(newRequest);
}
}
结果发现多此一举
我们点开@path这个注解的内部看一看
这里就是说,默认情况下是编码的 我们可以设置 encoded = true 禁用掉这个选项。
@FormUrlEncoded
@POST("{url}")
Observable<BaseBean<String>> login(@Path(value = "url", encoded = true) String url);
这样写,问题就解决了~
其他的问题还会陆续整理上来的…
注:参考了这篇文章
https://blog.youkuaiyun.com/yangxi_pekin/article/details/72421057。
最后:
这是本人第一次写博客,做了几年的Android开发,一直没总结过实在是失败,可能忙是理由,菜也是理由(还好知道自己菜),如果有什么问题的地方,请下方指正一下。