RxJava + Retrofit 应用整理

去年因为项目重构写了个 MVP 模式的框架 Demo,采用 RxJava + Retrofit 作为网路请求框架,一直想整理一下,但上半年确实浪得一(*),现在难得有点空闲,马上整理一下,作为回顾,查漏补缺。【听着怎么这么耳熟 ~ 】

相关文章:MVP(Model-View-Presenter)框架整理

一、框架搭建

网路请求框架基于以下版本:

// RXJava
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
// Retrofit
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
// okhttp的log信息
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
// JSON Parsing
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.squareup.retrofit2:converter-gson:2.2.0'
// Glide
compile 'com.github.bumptech.glide:glide:3.7.0'

import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * @author 小侨
 * @time 2017/7/21  14:52
 * @desc RetrofitHelper 类
 * 主要用来构造 retrofit 对象以及得到接口类对象
 */
public class RetrofitHelper {

    private static final RetrofitHelper mRetrofitHelper = new RetrofitHelper();
    private Retrofit mRetrofit;
    private OkHttpClient mClient;

    private RetrofitHelper() {
    	setInterceptor();
        setRetrofit();
    }

    public static RetrofitHelper getInstance(){
        return mRetrofitHelper;
    }

    private void setRetrofit() {
        mRetrofit = new Retrofit.Builder()
                .baseUrl("https://api.douban.com/")
                .addConverterFactory(GsonConverterFactory.create()) // 添加 gson 关联
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 添加 Rxjava 关联
                .client(mClient) // 设置 OkHttpClient
                .build();
    }
    
    /**
     * 设置 HttpLoggingInterceptor 消息拦截器,用于打印 URL、请求参数、响应结果
     */
    private void setInterceptor() {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                try {
                    String text = URLDecoder.decode(message, "utf-8");
                    LogUtil.http(text);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    LogUtil.http(message);
                }
            }
        });
        mClient = new OkHttpClient.Builder().addInterceptor(interceptor).build();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
    }

    public ApiService getApiService() {
        ApiService apiService = mRetrofit.create(ApiService.class);
        return apiService;
    }
}
import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Path;

/**
 * @author 小侨
 * @time 2017/7/21  14:44
 * @desc 数据接口类
 */
public interface ApiService {

    // 豆瓣250:http://api.douban.com/v2/movie/top250
    @GET("v2/movie/top250")
    Observable<Movies> getMovies250();

    // 条目信息:http://api.douban.com/v2/movie/subject/1764789
    @GET("v2/movie/subject/{id}")
    Observable<Movie> getMoviesById(@Path("id") int id);
}
import io.reactivex.disposables.CompositeDisposable;

/**
 * @author 小侨
 * @time 2017/7/21  10:11
 * @desc Presenter 基类
 */
public abstract class BasePresenter<V extends IView, M extends IModel> implements IPresenter {

    protected V mView;
    protected M mModel;
    // 用于取消订阅:http://blog.csdn.net/tyrantu1989/article/details/69053816
    // RxJava 2.0 CompositeDisposable 取代 CompositeSubscription
    protected CompositeDisposable mCompositeDisposable;

    public BasePresenter() {
        mModel = initModel();
        mCompositeDisposable = new CompositeDisposable();
    }

    protected abstract M initModel();

    @Override
    public void attachView(IView view) {
        mView = (V) view;
    }

    @Override
    public void detachView() {
        // activity 销毁的时候,切断所有订阅关系
        mCompositeDisposable.clear();
        mView = null;
    }
}

代码示例:

import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

/**
 * @author 小侨
 * @time 2017/7/21  10:25
 * @desc movie 页面 Presenter 层
 */
public class MoviePresenter extends BasePresenter<MovieActivity, MovieModel> implements MovieContract.IPresenter {

    // MoviePresenter<MovieActivity> 已经在 MovieActivity 中 onCreate 时绑定了
    // MoviePresenter<MovieModel> 已经在 MoviePresenter 新建时调用 initModel()方法绑定了
    @Override
    protected MovieModel initModel() {
        return new MovieModel();
    }

    @Override
    public void getTop250() {
        mModel.downloadTop250()
                .subscribeOn(Schedulers.io()) // 指定发射事件的线程
                .observeOn(AndroidSchedulers.mainThread()) // 指定消费事件的线程
                .subscribe(new Observer<Movies>() {
                    @Override
                    public void onSubscribe(Disposable d) {
	                    // 添加订阅事件到管理器,以便统一管理
                        mCompositeDisposable.add(d); 
                        mView.showLoading();
                        // TODO: 这里不用做 mView != null 判断,是因为在 BaseActivity 已经做了 onDestroy()时解绑了
                    }

                    @Override
                    public void onNext(Movies movies) {
                        mView.showTop250(movies);
                    }

                    @Override
                    public void onError(Throwable e) {
                        mView.hideLoading();
                        mView.showToast(e.getMessage());
                    }

                    @Override
                    public void onComplete() {
                        mView.hideLoading();
                    }
                });
    }

    @Override
    public void getMovieInfo(int id) {
        mModel.downloadMovieInfo(id)
                .subscribeOn(Schedulers.io()) // 另开一个线程来下载(指定一个观察者在哪个调度器上观察这个Observable)
                .observeOn(AndroidSchedulers.mainThread()) // 下载完回到主线程(指定 Observable 自身在哪个调度器上执行)
                .subscribe(new Observer<Movie>() {
                    ... ...
                });
    }
}
import io.reactivex.Observable;

/**
 * @author 小侨
 * @time 2017/7/21  10:30
 * @desc movie 页面 Model 层
 */
public class MovieModel implements IModel, MovieContract.IModel {

    @Override
    public Observable<Movies> downloadTop250() {
        ApiService apiService = RetrofitHelper.getInstance().getApiService();
        return apiService.getMovies250();
    }

    @Override
    public Observable<Movie> downloadMovieInfo(int id) {
        ApiService apiService = RetrofitHelper.getInstance().getApiService();
        return apiService.getMoviesById(id);
    }
}
import io.reactivex.Observable;

/**
 * @author 小侨
 * @time 2017/7/21  10:23
 * @desc movie 页面合约类:用于约束/定义 movie 页面的特有方法
 */
public class MovieContract {

    /**
     * View
     */
    interface IView {
        void showTop250(Movies movies);
        void showMovieInfo(Movie movie);
    }

    /**
     * Prezenter
     */
    interface IPresenter {
        void getTop250();
        void getMovieInfo(int id);
    }

    /**
     * Model
     */
    interface IModel {
        Observable<Movies> downloadTop250();
        Observable<Movie> downloadMovieInfo(int id);
    }
}
二、特殊返回码处理

实际开发中,后台有统一格式的返回 bean,比如当 code=200 时能成功获取到所需数据,其他 code 则要有对应的情形提示或者操作,我们不可能每次都去写这些判断,所以要抽取统一的处理逻辑:

import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;

/**
 * 抽取 http 返回结果处理类
 */

public abstract class HttpObserver<T extends BaseResultBean> implements Observer<T> {

    private BaseActivity mActivity;

    protected HttpObserver(BaseActivity activity) {
        this.mActivity = activity;
    }

    @Override
    public void onSubscribe(Disposable d) {
        mActivity.mCompositeDisposable.add(d);
        mActivity.showLoadingDialog();
    }

    @Override
    public void onNext(T t) {
        if (!"200".equals(t.getCode())) {

            // TODO token过期重新登录,暂时在这里验证
            if ("14003".equals(t.getCode()) || "14004".equals(t.getCode()) || "14005".equals(t.getCode())) {
                mActivity.showToast("token失效,请重新登录");
                Intent intent = new Intent(mActivity, LoginActivity.class);
                mActivity.startActivity(intent);
                ActivityManager.getInstance().finishOthersActivity(LoginActivity.class);
                return;
            }

            mActivity.showToast(t.getMessage());
            return;
        }
        onResult(t);
    }

    public abstract void onResult(T t);

    @Override
    public void onError(Throwable e) {
        mActivity.hideLoadingDialog();
        mActivity.showToast("网络错误");
    }

    @Override
    public void onComplete() {
        mActivity.hideLoadingDialog();
    }
}
/**
 * 请求返回结果 bean 基类
 */

public class BaseResultBean implements Serializable {

    private String code;
    private String message;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

抽取后的简单应用:

// 检查更新
// public class CheckNewReq extends BaseRequestBean 
// public class CheckNewRes extends BaseResultBean
    public void postCheckNew() {
        CheckNewReq request = new CheckNewReq();
        RetrofitHelper.getInstance().getApiService()
                .postCheckNew(request) // 参数
                .subscribeOn(Schedulers.io()) // 另开一个线程来下载
                .observeOn(AndroidSchedulers.mainThread()) // 下载完回到主线程
                .subscribe(new HttpObserver<CheckNewRes>(this) {
                    @Override
                    public void onResult(CheckNewRes info) {
                        ... ...
                    }
                });
    }
    // ApiService
    @POST("test/checknew")
    Observable<CheckNewRes> postCheckNew(@Body CheckNewReq requestBean);
三、文件上传

上面有常用的 POST 与 GET 方式的示例,下面贴出文件上传方式的示例:

    // ApiService
    /**
     * 图片上传
     */
    @Multipart
    @POST("test/upload")
    Observable<UploadImageRes> postUploadImage(
            @Query("memberId") long memberId, @Query("token") String token,
            @Part MultipartBody.Part file);

关键在于 @Multipart 注解及 RequestBody + MultipartBody.Part 的应用,均用于声明此请求方式为文件上传方式。

    public void postUploadImage(String imageUri, String filename) {
    	File file = new File(imageUri);
        RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        MultipartBody.Part body = MultipartBody.Part.createFormData("image", filename, requestFile);
        RetrofitHelper.getInstance().getUploadImageApiService()
                .postUploadImage(request.getMemberId(), request.getToken(), body)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new HttpObserver<UploadImageRes>(this) {
                    @Override
                    public void onResult(UploadImageRes info) {
                        ...
                    }
                });
    }

其中,name 是服务器开发人员定的 key 值,filename 是上传图片的文件名:

文件上传时遇到的问题:java.lang.IllegalArgumentException

原因是之前添加的的日志拦截,会拦截上传参数,如果是文件,拦截后会出现参数异常,识别不了,导致报错:

解决方式如下:不去拦截打印上传文件时的请求及响应体

        mClient = new OkHttpClient.Builder().addInterceptor(interceptor).build();
        // interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);

虽然,给上传文件请求配一个单独的日志拦截器可以解决这个问题,但是,如果需要请求及响应参数进行调试时,这个方式的弊端就显露出来了,所以要另找它法:

    public ApiService getUploadImageApiService() {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                try {
                    if (message.length() > 1000) {
                        return;
                    }
                    String text = URLDecoder.decode(message, "utf-8");
                    LogUtil.http(text);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    LogUtil.http(message);
                }
            }
        });
        OkHttpClient cient = new OkHttpClient.Builder().addInterceptor(interceptor).build();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://picture.zzq.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(cient)
                .build();
        return retrofit.create(ApiService.class);
    }

一大坨文件二进制转码:

上面的方法是可以了,但还是怪怪的,看后续有没有更好的解决方案,再做记录 ~


Demo 的 Github地址:
https://github.com/ZhangZeQiao/GeneralFramework.git
参考文章:
1、https://www.jianshu.com/p/1463bc223cd8
2、https://blog.csdn.net/huanglei201502/article/details/81185950

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值