去年因为项目重构写了个 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