上一篇,算是完成了准备工作,那么这篇就来说说MVP和RxJava的封装了。首先看看接口返回数据的格式:
{
"code" : 1,
"message" : "请求成功!" ,
"data" : {
"name": "张三",
"age": 3
}
}
code、message、data标准的三大门神。一般是以这种格式返回数据。数据格式的统一利于封装,以此数据格式为准的实体基类如下
在dataframework内新建包model和BaseResponseBean类。
package com.example.burro.demo.dataframework.model;
/**基类 泛型T为实体数据
* Created by ex.zhong on 2017/9/23.
*/
public class BaseResponseBean<T> {
private int code;
private String message;
private T data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
由于后面的demo用到豆瓣的API,很遗憾它的格式并非上述标准的格式。在项目中返回的数据用了继承父类方式,而非上面的泛型方式,为了区分。新的父类名字我改为BaseResultBean
,上面的标准格式基类我仍旧保存到demo中,如果更换的话,那也是分分钟的事情。后面案例和讲解也将使用BaseResultBean,
其内容如下:
package com.example.burro.demo.dataframework.model;
/**返回数据父类。子类可继承
* Created by ex.zhong on 2017/9/23.
*/
public class BaseResultBean {
protected int code;
protected String msg;
public BaseResultBean() {
}
public BaseResultBean(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
当然,项目中要根据实际数据为准来调整调整字段、结构等。没有必要过于纠结数据格式问题,换汤不换药,道理是一样的。
下面进行MVP相关内容的讲解!
依赖包引入:
项目的build.gradle增加如下appframework
/*rx-android-java*/
rxjava : 'io.reactivex:rxjava:1.1.0',
rxandroid : 'io.reactivex:rxandroid:1.1.0',
retrofit : 'com.squareup.retrofit2:retrofit:2.0.2',
converter_gson : 'com.squareup.retrofit2:converter-gson:2.0.2',
adapter_rxjava : 'com.squareup.retrofit2:adapter-rxjava:2.0.2',
//compile 'com.google.code.gson:gson:2.6.2'
logging_interceptor : 'com.squareup.okhttp3:logging-interceptor:3.3.0',
spots_dialog : 'com.github.d-max:spots-dialog:0.7@aar',
dataframework的build.gradle增加如下
compile deps.rxjava
compile deps.rxandroid
compile deps.retrofit
compile deps.converter_gson
compile deps.adapter_rxjava
compile deps.logging_interceptor
compile deps.spots_dialog
compile deps.annotation
BaseView
写之前需要在
appframework
下新建包mvp
,mvp下新建三个包contract
,presenter
,view
在view下新建接口
BaseView
,Baseview接口内的方法是页面内【Activity或者Fragment】需要执行的通用方法。这里先定义一个 showError(BaseResultBean resultBean);
返回正确情况有很多种,在实现类中增加,若错误,我们要统一处理。所以showError(BaseResultBean resultBean)方法是全局共有的。
package com.example.burro.demo.appframework.mvp.view;
import com.example.burro.demo.dataframework.model.BaseResultBean;
/**View接口
* Created by ex.zhong on 2017/9/23.
*/
public interface BaseView {
void showError(BaseResultBean resultBean);
}
BasePresenter
Presenter和View创建类似,
在presenter下新建IPresenter
,IPresenter attachView(T view); void detachView();两个方法是全局共有的
package com.example.burro.demo.appframework.mvp.presenter;
import com.example.burro.demo.appframework.mvp.view.BaseView;
/**Presenter接口
* 注:在创建presenter时绑定,在页面destroy()时解绑。
* Created by ex.zhong on 2017/9/23.
*/
public interface IPresenter<T extends BaseView> {
void attachView(T view);
void detachView();
}
因为几乎每个Presenter实现类里都要处理绑定和解绑事件,所以我们要把这个处理过程提取出来,此处写一个基类BasePresenter统一管理,在presenter下新建BasePresenter
实现IPresenter
package com.example.burro.demo.appframework.mvp.presenter;
import com.example.burro.demo.appframework.mvp.view.BaseView;
/**
* Presenter基类。目的是统一处理绑定和解绑
* Created by ex.zhong on 2017/9/23.
*/
public class BasePresenter<T extends BaseView> implements IPresenter<T> {
protected T mView;
@Override
public void attachView(T mView) {
mView = mView;
}
@Override
public void detachView() {
mView = null;
}
// public boolean isViewAttached() {
// return mView != null;
// }
// public void checkViewAttached() {
// if (!isViewAttached()) throw new
// MvpViewNotAttachedException();
// }
![Uploading 04_766476.png . . .]
// public static class //MvpViewNotAttachedException extends //RuntimeException {
// public MvpViewNotAttachedException() {
// super("Please call //Presenter.attachView(MvpView) before" +
// " requesting data to the //Presenter");
// }
// }
}
【备注:这里的checkViewAttached(),在rxJava未引入之前使用。目的是判断页面是否还存在,若不存在则不执行。rxJava中对此作了处理。只需调用解绑方法即可。在此处稍作提及,后面我会直接删掉此内容】
稍后会写一个测试类TestActivty结合豆瓣的API。来详解mvp的使用。在此之前先来封装一下BaseActivity,因为一般都是在BaseActivity中进行Presenter和View的初始化绑定
BaseActivity
在appframework新建ui包,包内新建BaseActivity抽象类
类中都是基本的要素,且注释较为详细,容易理解。其中有个别方法是为了和后面内容对接,直接贴代码:
package com.example.burro.demo.appframework.ui;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import com.example.burro.demo.appframework.BaseApplication;
import com.example.burro.demo.appframework.mvp.presenter.BasePresenter;
import com.example.burro.demo.appframework.mvp.view.BaseView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
/**
* BaseActivity Activity基类
* butterKnife的绑定 初始方法的设定 presentet和view的绑定
* Created by ex.zhong on 2017/9/23.
*/
public abstract class BaseActivity<T extends BasePresenter> extends AppCompatActivity implements BaseView,Toolbar.OnMenuItemClickListener {
protected T mPresenter;
protected Activity mContext;
private Unbinder mUnbinder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(initLayoutInflater());
mUnbinder = ButterKnife.bind(this);
mContext = this;
createPresenter();
if (mPresenter != null) mPresenter.attachView(this);
BaseApplication.getInstance().addActivity(this);
initParams();
initViews();
}
protected abstract int initLayoutInflater(); //初始化布局
protected abstract void initParams(); //初始化参数
protected abstract void initViews(); //初始化控件
protected abstract void createPresenter(); //创建presenter
/**
* @param toolbar toolbar 控件
* @param title 标题
*/
protected void setToolBar(Toolbar toolbar, String title) {
if (toolbar != null) {
if (title != null) toolbar.setTitle(title);
setSupportActionBar(toolbar);
toolbar.setOnMenuItemClickListener(this);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onBackPressed();
}
});
}
}
//toolbar右侧menu点击事件
@Override
public boolean onMenuItemClick(MenuItem item) {
return false;
}
//统一处理错误信息
public void handleError(BaseResultBean errResult) {
if (errResult == null) return;
if (this == null) return;
//可以分门别类的处理 错误消息,如session过期,跳转到登录页面。其他情况提示即可
ToastUtils.showToast(mContext, errResult.getMsg());
}
@Override
protected void onDestroy() {
if (mPresenter != null) mPresenter.detachView();
if (mUnbinder != null) mUnbinder.unbind();
super.onDestroy();
}
}
在biz新建测试类
新建内容如下 biz/test/view/TestActivity、biz/test/TestContract、biz/test/TestPresenterImpl
1.包内新建TestActivity继承BaseActivity.TestActivity
package com.example.burro.demo.appbiz.test.view;
import com.example.burro.demo.appbiz.R;
import com.example.burro.demo.appbiz.R2;
import com.example.burro.demo.appbiz.test.TestContract;
import com.example.burro.demo.appbiz.test.TestPresenterImpl;
import com.example.burro.demo.appframework.ui.BaseActivity;
import com.example.burro.demo.appframework.util.LogUtils;
import com.example.burro.demo.databiz.model.test.MovieListBean;
import com.example.burro.demo.dataframework.model.BaseResultBean;
import butterknife.OnClick;
/**测试页面
* Created by ex.zhong on 2017/9/23.
*/
public class TestActivity extends BaseActivity<TestPresenterImpl> implements TestContract.View{
@Override
protected int initLayoutInflater() {
return R.layout.activity_test;
}
@Override
protected void initParams() {
}
@Override
protected void initViews() {
}
@Override
protected void createPresenter() {
mPresenter = new TestPresenterImpl();
}
@Override
public void showError(BaseResultBean resultBean) {
//错误处理
handleError(resultBean);
}
@Override
public void setMovieListData(MovieListBean bean) {
LogUtils.i("TAG",bean==null?"":bean.toString());
}
@OnClick(R2.id.btnTest)
public void getMovieListData(){
mPresenter.getMovieListData(1,15);
}
}
这里的showError(),是BasePresenter中的回调方法,用来统一处理错误情况,若页面有RecycalView并正在刷新的情况,也可在此处结束刷新。因为每个页面都会showError(),所以我们需要在BaseActivity里增加统一处理的方法handleError(resultBean),内容如下:
//统一处理错误信息
public void handleError(BaseResultBean errResult) {
if (errResult == null) return;
if (this == null) return;
//可以分门别类的处理 错误消息,如session过期,跳转到登录页面。其他情况提示即可
ToastUtils.showToast(mContext, errResult.getMsg());
}
值得强调的是,增加错误结果统一处理很有必要,也很少有人注意这点,我们后面网络请求错误结果的返回也会与此对接。此处默认是给出Toast提示信息,当然还有很多其他操作,正如注释所说:如果errResult的code是session过期的标识,那么我们给出提示的同时也会跳转至登录页面等等。
2.TestContract:
Contract:d单词意思为契约、协议。TestContract即协议类,定制mvp各层接口和实现方法。说白了,就是把v层和p层需要实现的方法统一在一块,方便管理,也起到了解耦作用。
package com.example.burro.demo.appbiz.test;
import com.example.burro.demo.appframework.mvp.presenter.IPresenter;
import com.example.burro.demo.appframework.mvp.view.BaseView;
import com.example.burro.demo.databiz.model.test.MovieListBean;
/**协议类,定制mvp各层接口和实现方法
* Contract:d单词意思为契约 协议
* 接口View内 定义实现view内所需方法
* 接口Presenter 定义实现presenter内所需的方法
* Created by ex.zhong on 2017/9/23.
*/
public class TestContract {
public interface View extends BaseView {
void setMovieListData(MovieListBean bean);
}
public interface Presenter extends IPresenter<View> {
void getMovieListData(int start,int count);
}
}
3.TestPresenterImpl:
TestPresenterImpl继承自BasePresenter,初级版本如下:
package com.example.burro.demo.appbiz.test;
import com.example.burro.demo.appframework.mvp.presenter.BasePresenter;
import com.example.burro.demo.appbiz.test.TestContract.*;
import com.example.burro.demo.appframework.util.LogUtils;
import com.example.burro.demo.appframework.util.StringUtils;
import com.example.burro.demo.databiz.model.test.MovieListBean;
import com.example.burro.demo.databiz.service.ApiService;
import com.example.burro.demo.dataframework.http.HttpConfig;
import java.util.HashMap;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
/**测试presenter
* Created by ex.zhong on 2017/9/23.
*/
public class TestPresenterImpl extends BasePresenter<View> implements Presenter {
@Override
public void getMovieListData(int start, int count) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(HttpConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
ApiService movieService = retrofit.create(ApiService.class);
HashMap<String,String> map=new HashMap<>();
map.put("start", StringUtils.getString(start));
map.put("count",StringUtils.getString(count));
movieService.getMovieListData(map)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<MovieListBean>() {
@Override
public void onStart() {
//请求开始
LogUtils.i("TestPresenterImpl","onStart()");
}
@Override
public void onCompleted() {
// //请求完成
LogUtils.i("TestPresenterImpl","onCompleted()");
}
@Override
public void onError(Throwable e) {
// //请求异常
LogUtils.i("TestPresenterImpl","onError()");
}
@Override
public void onNext(MovieListBean movieListBean) {
// //请求OK,执行
LogUtils.i("TestPresenterImpl","onNext()");
mView.setMovieListData(movieListBean);
}
});
}
}
文中用到的豆瓣电影TOP250的URL为:http://api.douban.com/v2/movie/top250?start=1&count=15
其他几个主要辅助的类或资源分别为如下:
布局文件activity_test.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.burro.demo.appbiz.test.view.TestActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<Button
android:id="@+id/btnTest"
android:layout_below="@id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="请求数据"
/>
</RelativeLayout>
其中AppTheme.PopupOver,AppTheme.AppBarOverlay等是toolbar相关的样式资源,请到demo中查看,此处不一一列出
ApiService接口类 databiz/service/ApiService:
package com.example.burro.demo.databiz.service;
import com.example.burro.demo.databiz.model.test.MovieListBean;
import java.util.HashMap;
import java.util.Map;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
import rx.Observable;
/**
* 存放访问网络的方法
* Created by ex.zhong on 2017/9/24.
*/
public interface ApiService {
public static final String URL_MOVIELIST="/v2/movie/top250"; //豆瓣电影top250
@GET(URL_MOVIELIST)
Observable<MovieListBean> getMovieListData(@QueryMap HashMap<String,String> count);
}
网络配置类 dataframework/http/HttpConfig :
package com.example.burro.demo.dataframework.http;
/**
* Created by ex.zhong on 2017/9/24.
*放置网络相关配置数据,如IP/端口等
*/
public class HttpConfig {
public final static String BASE_URL="http://api.douban.com";
}
电影列表实体类 databiz/model/test/MovieListBean :
package com.example.burro.demo.databiz.model.test;
import com.example.burro.demo.dataframework.model.BaseResultBean;
import java.util.List;
/**豆瓣电影列表
* Created by ex.zhong on 2017/9/24.
*/
public class MovieListBean extends BaseResultBean{
public List<SubjectsBean> subjects;
public static class SubjectsBean {
/**
* rating : {"max":10,"average":9.6,"stars":"50","min":0}
* genres : ["犯罪","剧情"]
* title : 肖申克的救赎
* casts : [{"alt":"https://movie.douban.com/celebrity/1054521/","avatars":{"small":"https://img3.doubanio.com/img/celebrity/small/17525.jpg","large":"https://img3.doubanio.com/img/celebrity/large/17525.jpg","medium":"https://img3.doubanio.com/img/celebrity/medium/17525.jpg"},"name":"蒂姆·罗宾斯","id":"1054521"},{"alt":"https://movie.douban.com/celebrity/1054534/","avatars":{"small":"https://img3.doubanio.com/img/celebrity/small/34642.jpg","large":"https://img3.doubanio.com/img/celebrity/large/34642.jpg","medium":"https://img3.doubanio.com/img/celebrity/medium/34642.jpg"},"name":"摩根·弗里曼","id":"1054534"},{"alt":"https://movie.douban.com/celebrity/1041179/","avatars":{"small":"https://img1.doubanio.com/img/celebrity/small/5837.jpg","large":"https://img1.doubanio.com/img/celebrity/large/5837.jpg","medium":"https://img1.doubanio.com/img/celebrity/medium/5837.jpg"},"name":"鲍勃·冈顿","id":"1041179"}]
* collect_count : 1107705
* original_title : The Shawshank Redemption
* subtype : movie
* directors : [{"alt":"https://movie.douban.com/celebrity/1047973/","avatars":{"small":"https://img3.doubanio.com/img/celebrity/small/230.jpg","large":"https://img3.doubanio.com/img/celebrity/large/230.jpg","medium":"https://img3.doubanio.com/img/celebrity/medium/230.jpg"},"name":"弗兰克·德拉邦特","id":"1047973"}]
* year : 1994
* images : {"small":"https://img3.doubanio.com/view/movie_poster_cover/ipst/public/p480747492.webp","large":"https://img3.doubanio.com/view/movie_poster_cover/lpst/public/p480747492.webp","medium":"https://img3.doubanio.com/view/movie_poster_cover/spst/public/p480747492.webp"}
* alt : https://movie.douban.com/subject/1292052/
* id : 1292052
*/
public RatingBean rating;
public String title;
public int collect_count;
public String original_title;
public String subtype;
public String year;
public ImagesBean images;
public String alt;
public String id;
public List<String> genres;
public List<CastsBean> casts;
public List<DirectorsBean> directors;
public static class RatingBean {
/**
* max : 10
* average : 9.6
* stars : 50
* min : 0
*/
public int max;
public double average;
public String stars;
public int min;
}
public static class ImagesBean {
/**
* small : https://img3.doubanio.com/view/movie_poster_cover/ipst/public/p480747492.webp
* large : https://img3.doubanio.com/view/movie_poster_cover/lpst/public/p480747492.webp
* medium : https://img3.doubanio.com/view/movie_poster_cover/spst/public/p480747492.webp
*/
public String small;
public String large;
public String medium;
}
public static class CastsBean {
/**
* alt : https://movie.douban.com/celebrity/1054521/
* avatars : {"small":"https://img3.doubanio.com/img/celebrity/small/17525.jpg","large":"https://img3.doubanio.com/img/celebrity/large/17525.jpg","medium":"https://img3.doubanio.com/img/celebrity/medium/17525.jpg"}
* name : 蒂姆·罗宾斯
* id : 1054521
*/
public String alt;
public AvatarsBean avatars;
public String name;
public String id;
public static class AvatarsBean {
/**
* small : https://img3.doubanio.com/img/celebrity/small/17525.jpg
* large : https://img3.doubanio.com/img/celebrity/large/17525.jpg
* medium : https://img3.doubanio.com/img/celebrity/medium/17525.jpg
*/
public String small;
public String large;
public String medium;
}
}
public static class DirectorsBean {
/**
* alt : https://movie.douban.com/celebrity/1047973/
* avatars : {"small":"https://img3.doubanio.com/img/celebrity/small/230.jpg","large":"https://img3.doubanio.com/img/celebrity/large/230.jpg","medium":"https://img3.doubanio.com/img/celebrity/medium/230.jpg"}
* name : 弗兰克·德拉邦特
* id : 1047973
*/
public String alt;
public AvatarsBeanX avatars;
public String name;
public String id;
public static class AvatarsBeanX {
/**
* small : https://img3.doubanio.com/img/celebrity/small/230.jpg
* large : https://img3.doubanio.com/img/celebrity/large/230.jpg
* medium : https://img3.doubanio.com/img/celebrity/medium/230.jpg
*/
public String small;
public String large;
public String medium;
}
}
}
}
点击请求数据按钮。获取到返回的数据如下:
当然,TestPresenterImpl中的内容是重点,其中getMovieListData()方法里的内容是retrofit和rxjava最基本的用法!想必大家多少都见过。我们再来看下rxjava相关的代码,其实它主要做了三个事情。统一管理主线程、工作线程、请求返回后的回调处理!引入rxjava之前,三者都是自己管理。所以说,它的引入极大的简化了我们的工作。
下一篇将讲述优化封装