[Android开发] RxJava2 +Retrofit2 + okhttp3 网络框架的简单集成

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.csdn.net/yangxi_pekin/article/details/72421057。

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值