[Android开发] RxJava2 +Retrofit2 + okhttp3 网络框架的简单集成
发布日期:2022-03-02 13:23:55 浏览次数:46 分类:技术文章

本文共 18570 字,大约阅读时间需要 61 分钟。

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
interceptors, List
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
interceptors) {
return getInstance(baseUrl, interceptors, null); } public static RetrofitServiceManager getInstance(String baseUrl, List
interceptors, List
factories) {
if (serviceManager == null) {
synchronized (RetrofitServiceManager.class) {
if (serviceManager == null) {
serviceManager = new RetrofitServiceManager(baseUrl, interceptors, factories); } } } return serviceManager; } public
T create(Class
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
Observable
useThread( Observable
observable) {
return observable //指定被观察者执行的线程 .subscribeOn(Schedulers.io()) .unsubscribeOn(Schedulers.io()) //指定观察者执行的线程(我们这里通常是这样的操作,当然我们还可以在Task中自行切换线程) .observeOn(AndroidSchedulers.mainThread()); } //将键值对的参数转换成JsonBody public RequestBody getJSonBody(Map
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
*用于文件上传一类的操作 **/ public Map
convertToPartMap(Map
map) { Map
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
login(Map
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
> login(@FieldMap Map
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
{
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
login(Map
map) {
return useThread(context, loginService.login(map) .flatMap(new Function
, ObservableSource
>() {
@Override public ObservableSource
apply(BaseBean
loginBeanBaseBean) throws Exception {
if (!loginBeanBaseBean.isCode()) {
throw new BusinessException(loginBeanBaseBean.getMsg()); } return Observable.just(loginBeanBaseBean.getData()); } })); }

2.语法糖

public Observable
login(Map
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 * 控制加载动画,并拦截了网络和业务异常流程, * 使用时只关心结果和异常流程 */@MainThreadpublic abstract class NetworkObserver
extends DisposableObserver
{
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
params = new HashMap(); params.put("username", username); params.put("password", password); Disposable disposable = loginTask.login(params) .subscribeWith(new NetworkObserver
(this) {
@Override public void onSuccess(LoginBean loginBean) {
...... } @Override public void onError(ResultException e) {
...... } }); addDisposable(disposable);

这样的网络请求是不是很简洁明了啊。

还有就是使用这个框架时的遇到的坑:

1. Retrofit2动态设置URL遇到的问题

@FormUrlEncoded        @POST("user/{url}")        Observable
> login(@Path("url") String url);

上面这种写法是url中只需要替换一个部分的情况,但是有时候我们需要整个POST中的url都替换了,比如说这样

@FormUrlEncoded        @POST("{url}")        Observable
> 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
> login(@Path(value = "url", encoded = true) String url);

这样写,问题就解决了~

其他的问题还会陆续整理上来的…

注:参考了这篇文章

https://blog.csdn.net/yangxi_pekin/article/details/72421057。

最后:

这是本人第一次写博客,做了几年的Android开发,一直没总结过实在是失败,可能忙是理由,菜也是理由(还好知道自己菜),如果有什么问题的地方,请下方指正一下。

转载地址:https://blog.csdn.net/AdminStar/article/details/102605280 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:[Android Studio4.1]项目的导出ZIP位置更新
下一篇:Typora入门使用说明书

发表评论

最新留言

网站不错 人气很旺了 加油
[***.192.178.218]2024年04月09日 11时10分05秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章