转载请指明出处:http://blog.csdn.net/lupengfei1009/article/details/50989066
这段时间看了不少基于MVP设计模式,然后结合RxJava+Retrofit写的开源项目,深受感触,为了能让更多像我这种基层码畜也能够体验一把大神们的世界,下面分享一点学习经验。
什么是MVP
-
model
处理业务逻辑,主要是数据读写,或者与后台通信,说通俗点就是取数据的地方。 -
view
用于更新UI,由于Android中与用户交互的只要是activity或fragment,所以,view一般就是值activity或fragment -
presenter
代理,用于协调管理model和view,通知model获取数据,model获取数据完之后,通知view更新界面
为什么要用MVP
使用有一个最大的好处就是解耦,view就只负责更新UI,显示控件,完成与用户的交互;model的职责呢就是去加载数据;具体的model什么时候去获取数据,获取完了之后ui什么时候去更新,这一切都是由presenter去完成。这样做,一方面适合团队协作去开发,另一方面也方便测试,各个模块之间互不干扰。还有更多的好处和优点请自行百度。
怎么去完成一个MVP的设计呢
所谓的MVP,其实说通俗一点就是将功能拆分成各个模块,各自完成之后通过回调去做数据交互。前端(activity且已经实现了view接口)在实例化presenter的时候会将自己的view接口回调告诉它(presenter),此时UI的管理权就交给了presenter;presenter在实例化model对象的实例的时候,会将自己的接口回调传递给model对象(有人会说,这样就其实是把管理presenter的管理权交给了model,为什么这么写呢,因为网络请求都是需要耗时的,如果在presenter里面去调用model方法,并等待返回的话,会造成线程阻塞;如果不是耗时的操作,那么就可以在model中暴露方法让presenter去获取数据),model在获取完数据之后,通过presenter的回调告诉他,presenter在接受到model的数据之后又通过先前持有的view的回调将数据或者提示等信息告知UI并更新。如下图:
接下来,我以一个通过接口获取号码归属地的例子去剖析一下MVP,同时简单结合RxJava+Retrofit,最终效果如下图。
demo实例,功能虽小,五脏俱全
准备工作
-
项目目录结构,如下图
-
获取号码归属地的接口及回复的说明
接口地址:http://api.k780.com:88/?app=phone.get&phone=13888888888&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json
成功返回数据如下:
{
“success”: “1”,
“result”: {
“status”: “ALREADY_ATT”,
“phone”: “13888888888”,
“area”: “0871”,
“postno”: “650000”,
“att”: “中国,云南,昆明”,
“ctype”: “中国移动138卡”,
“par”: “1388888”,
“prefix”: “138”,
“operators”: “中国移动”,
“style_simcall”: “中国,云南,昆明”,
“style_citynm”: “中华人民共和国,云南省,昆明市”
}
}失败返回:
{
“success”: “0”,
“msgid”: “1000801”,
“msg”: “手机号码不正确”
}
注:以上测试电话号码为任意输入的测试号码,无任何其他用意,还请机主能谅解。 -
定义收据归属地的实体类
以下的Bean是GsonFormat插件自动生成,还没有使用的朋友可以尝试一下,很不错的一款android studio插件
import com.lpf.mvptest.base.BaseBean; /** * 电话号码的归属地及其他信息的对象 * Created by Administrator on 2016/3/23. */ public class PhoneNumInfo extends BaseBean { private ResultEntity result; /** * msg : 手机号码不正确 * msgid : 1000801 */ private String msg; private String msgid; public ResultEntity getResult() { return result; } public void setResult(ResultEntity result) { this.result = result; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getMsgid() { return msgid; } public void setMsgid(String msgid) { this.msgid = msgid; } public static class ResultEntity { private String status; private String phone; private String area; private String postno; private String att; private String ctype; private String par; private String prefix; private String operators; private String style_simcall; private String style_citynm; public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getArea() { return area; } public void setArea(String area) { this.area = area; } public String getPostno() { return postno; } public void setPostno(String postno) { this.postno = postno; } public String getAtt() { return att; } public void setAtt(String att) { this.att = att; } public String getCtype() { return ctype; } public void setCtype(String ctype) { this.ctype = ctype; } public String getPar() { return par; } public void setPar(String par) { this.par = par; } public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getOperators() { return operators; } public void setOperators(String operators) { this.operators = operators; } public String getStyle_simcall() { return style_simcall; } public void setStyle_simcall(String style_simcall) { this.style_simcall = style_simcall; } public String getStyle_citynm() { return style_citynm; } public void setStyle_citynm(String style_citynm) { this.style_citynm = style_citynm; } @Override public String toString() { return "ResultEntity{" + "status='" + status + '\'' + ", phone='" + phone + '\'' + ", area='" + area + '\'' + ", postno='" + postno + '\'' + ", att='" + att + '\'' + ", ctype='" + ctype + '\'' + ", par='" + par + '\'' + ", prefix='" + prefix + '\'' + ", operators='" + operators + '\'' + ", style_simcall='" + style_simcall + '\'' + ", style_citynm='" + style_citynm + '\'' + '}'; } } @Override public String toString() { return "PhoneNumInfo{" + "success='" + success + '\'' + ", result=" + result + '}'; } }
View的基类
-
构建view的基础接口
一个界面,请求一次数据基本都分为下面几个步骤:显示加载框–>加载数据成功(加载失败)–>更新UI(提示用户)–>关闭正在加载的框这么5个事情,那么我们就定义一个需要做这5件事儿的接口,由于是基类,所以返回的对象由具体的业务子类去定义就好。最终这个类的实现我们在activity或者fragment中去完成,以下为view接口的基类/** * 视图(View层)基础回调接口 */ public interface IBaseView<T> { /** * 通过toast提示用户 * * @param msg 提示的信息 * @param requestTag 请求标识 */ void toast(String msg, int requestTag); /** * 显示进度 * * @param requestTag 请求标识 */ void showProgress(int requestTag); /** * 隐藏进度 * * @param requestTag 请求标识 */ void hideProgress(int requestTag); /** * 基础的请求的返回 * * @param data * @param requestTag 请求标识 */ void loadDataSuccess(T data, int requestTag); /** * 基础请求的错误 * * @param e * @param requestTag 请求标识 */ void loadDataError(Throwable e, int requestTag); }
presenter的基类
-
定义代理(presenter)回调接口
/** * 请求数据的回调<br> * Presenter用于接受model获取(加载)数据后的回调 * Created by Administrator on 2016/3/23. */ public interface IBasePresenter<T> { /** * 开始请求之前 */ void beforeRequest(int requestTag); /** * 请求失败 * * @param e 失败的原因 */ void requestError(Throwable e, int requestTag); /** * 请求结束 */ void requestComplete(int requestTag); /** * 请求成功 * * @param callBack 根据业务返回相应的数据 */ void retuestSuccess(T callBack, int requestTag); }
-
写一个presenter的具体实现的基础类BasePresenterImpl
或许会问,具体的实现放到具体的presenter的业务中去写不就好了嘛,何必要在这里写一遍呢,又做不了什么事情。NO!NO!NO!你想错了,是否还记得前面定义的IBaseView,里面定义了一些基本的UI操作;在这个BasePresenterImpl中,我们可以做一些基础的事情(所有请求都会有的,比如:打开Loading弹框、加载失败后的错误提示),那么就不用在每个子类里面都要去写一次。同时这个类接受2个泛型T,用于分别指定View视图(T)及请求返回的结果(V)
/** * 代理对象的基础实现 * * @param <T> 视图接口对象(view) 具体业务各自继承自IBaseView * @param <V> 业务请求返回的具体对象 */ public class BasePresenterImpl<T extends IBaseView, V> implements IBasePresenter<V> { public IBaseView iView; /** * 构造方法 * * @param view 具体业务的接口对象 */ public BasePresenterImpl(T view) { this.iView = view; } @Override public void beforeRequest(int requestTag) { //显示LOading iView.showProgress(requestTag); } @Override public void requestError(Throwable e, int requestTag) { //通知UI具体的错误信息 iView.loadDataError(e,requestTag); } @Override public void requestComplete(int requestTag) { //隐藏Loading iView.hideProgress(requestTag); } @Override public void retuestSuccess(V callBack, int requestTag) { //将获取的数据回调给UI(activity或者fragment) iView.loadDataSuccess(callBack, requestTag); } }
代码中可以看到,加载前的弹框,加载成功回调给UI,加载失败通知UI错误信息,加载完成关闭弹框等都已经在这里做了一个基础的实现。如果其中的方法不能满足你的业务需求,你可以在具体业务的presenter实现中去重写相应方法添加具体缺失的实现。
Model的基类
-
业务(model)类的基类
其中有写到RetrofitManager这个类,在下面将会介绍,这是一个用于初始化retrofit和service的类,也就是model的辅助对象。
/** * 业务对象的基类 */ public class BaseModel { //retrofit请求数据的管理类 public RetrofitManager retrofitManager; public BaseModel() { //初始化retrofit retrofitManager = RetrofitManager.builder(); } }
-
定义一个请求归属地的服务
/** * 归属地请求的服务 */ public interface PhoneNunInfoService { //http://api.k780.com:88/?app=phone.get&phone={phoneNum}&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json @Headers(RetrofitManager.CACHE_CONTROL_AGE + RetrofitManager.CACHE_STALE_LONG) @GET("/") Observable<PhoneNumInfo> getBeforeNews(@Query("app") String app , @Query("phone") String phone , @Query("appkey") String appkey , @Query("sign") String sign , @Query("format") String format); }
-
创建RetrofitManager对象,他的作用就是初始化retrofit、service以及添加缓存机制(如果不理解可以不关注他,将那一块的代码注释掉依然是可以运行的)
/** * Retrofit管理类 */ public class RetrofitManager { //地址 public static final String BASE_PHONENUMINFO_URL = "http://api.k780.com:88"; //短缓存有效期为1分钟 public static final int CACHE_STALE_SHORT = 60; //长缓存有效期为7天 public static final int CACHE_STALE_LONG = 60 * 60 * 24 * 7; public static final String CACHE_CONTROL_AGE = "Cache-Control: public, max-age="; //查询缓存的Cache-Control设置,为if-only-cache时只查询缓存而不会请求服务器,max-stale可以配合设置缓存失效时间 public static final String CACHE_CONTROL_CACHE = "only-if-cached, max-stale=" + CACHE_STALE_LONG; //查询网络的Cache-Control设置,头部Cache-Control设为max-age=0时则不会使用缓存而请求服务器 public static final String CACHE_CONTROL_NETWORK = "max-age=0"; private static OkHttpClient mOkHttpClient; private final PhoneNunInfoService phoneNunInfoService; public static RetrofitManager builder() { return new RetrofitManager(); } public PhoneNunInfoService getService() { return phoneNunInfoService; } private RetrofitManager() { initOkHttpClient(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_PHONENUMINFO_URL) .client(mOkHttpClient) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); phoneNunInfoService = retrofit.create(PhoneNunInfoService.class); } private void initOkHttpClient() { HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); if (mOkHttpClient == null) { synchronized (RetrofitManager.class) { if (mOkHttpClient == null) { // 指定缓存路径,缓存大小100Mb Cache cache = new Cache(new File(MyApplication.getContext().getCacheDir(), "HttpCache"), 1024 * 1024 * 100); mOkHttpClient = new OkHttpClient.Builder() .cache(cache) .addInterceptor(mRewriteCacheControlInterceptor) .addNetworkInterceptor(mRewriteCacheControlInterceptor) .addInterceptor(interceptor) .addNetworkInterceptor(new StethoInterceptor()) .retryOnConnectionFailure(true) .connectTimeout(15, TimeUnit.SECONDS) .build(); } } } } // 云端响应头拦截器,用来配置缓存策略 private Interceptor mRewriteCacheControlInterceptor = new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); if (!NetUtil.isNetworkConnected()) { request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build(); } Response originalResponse = chain.proceed(request); if (NetUtil.isNetworkConnected()) { //有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置 String cacheControl = request.cacheControl().toString(); return originalResponse.newBuilder().header("Cache-Control", cacheControl) .removeHeader("Pragma").build(); } else { return originalResponse.newBuilder() .header("Cache-Control", "public, only-if-cached, max-stale=" + CACHE_STALE_LONG) .removeHeader("Pragma").build(); } } }; }
到此,基础的东西已经完成了,这样的事情,虽然说在实现这样的小功能中确实是显的有些多余了,但是在实际的开发过程中,我们并不是只做一件事,只做一个请求,只显示一个界面,因此,这样往上抽一层还是很有必要的。下面将回归到真正写号码归属地的实际业务中来
具体的业务实现
-
查询号码归属地的view
由于基础的view接口已经可以满足号码归属地查询的ui更新使用了,同时也为了项目方便管理,便于理解,我们新建一个PhoneNumInfoView,继承IBaseView,并告诉baseView请求成功后需要返回一个PhoneNumInfo的对象。
/** * 号码归属地查询的View */ public interface PhoneNumInfoView extends IBaseView<PhoneNumInfo>{ }
-
定义获取归属地的Model
/** * 获取号码归属地的具体Model实现 */ public class PhoneModelImpl extends BaseModel { private Context mContext; private PhoneNunInfoService phoneNunInfoService; public PhoneModelImpl(Context context) { super(); this.mContext = context; phoneNunInfoService = retrofitManager.getService(); } public void loadPhoneNumInfo(String phoneNum, final IBasePresenter<PhoneNumInfo> callBack, final int requestTag) { phoneNunInfoService.getBeforeNews("phone.get", phoneNum, "10003", "b59bc3ef6191eb9f747dd4e83c99f2a4", "json") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<PhoneNumInfo>() { @Override public void onStart() { super.onStart(); callBack.beforeRequest(requestTag); } @Override public void onCompleted() { callBack.requestComplete(requestTag); } @Override public void onError(Throwable e) { callBack.requestError(e, requestTag); } @Override public void onNext(PhoneNumInfo phoneNumInfo) { if (null != phoneNumInfo && phoneNumInfo.getSuccess().equals("1")) callBack.retuestSuccess(phoneNumInfo, requestTag); else if (null != phoneNumInfo && phoneNumInfo.getSuccess().equals("0")) callBack.requestError(new Exception(phoneNumInfo.getMsg()), requestTag); else callBack.requestError(new Exception("获取数据错误,请重试!"), requestTag); } }); } }
-
定义一个用于获取号码归属地的代理对象(presenter)
在实例化代理对象的时候拿到前端(activity或者fragment)的view实例,并初始化Model对象,同时对外提供一个获取数据的方法,方便activity去获取数据。
/** * Presenter的实现,协调model去加载数据,获取model加载完成时候的回调,控制界面加载框的显示与隐藏 */ public class PhonePresenterImpl extends BasePresenterImpl<PhoneNumInfoView, PhoneNumInfo> { private PhoneModelImpl phoneModel; private Context mContext; public PhonePresenterImpl(PhoneNumInfoView phoneNumInfoView, Context context) { super(phoneNumInfoView); this.mContext = context; phoneModel = new PhoneModelImpl(mContext); } /** * 获取归属地信息 * * @param phoneNum 电话号码 * @param requestTag 请求标识 */ public void getPhoneNumInfo(String phoneNum, int requestTag) { phoneModel.loadPhoneNumInfo(phoneNum, this, requestTag); } }
-
实现activity中相关的代码
前端activity的代码并不多,就是实现PhoneNumInfoView的相应接口,然后最基础的获取控件,实例化presenter,把当前的view实例以参数的形式传递给presenter;接口的实现也就是要显示Loading的地方把框show出来,在获取数据成功的地方显示数据等等,各负其职。然后在按钮的点击事件的地方调用presenter中相应的获取数据的方法。具体的什么时候显示Loading,什么时候执行加载完成的操作等,就不需要activity去管了,安安心心的全权交个presenter去做就好了。代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener, PhoneNumInfoView { //获取归属地的请求标识,用回页面多个地方请求的时候,标识是那一个完成并回调了 private static final int REQUESTMSG = 0; //电话号码的EditText private EditText phoneNum; //获取归属地的按钮 private Button getPhoneInfo; //用于显示最后获取的结果 private TextView msg; //Loading弹框 private ProgressDialog progressDialog; //获取归属地的代理对象 private PhonePresenterImpl phonePresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { //初始化控件 phoneNum = (EditText) findViewById(R.id.phonenum); getPhoneInfo = (Button) findViewById(R.id.getphoneinfo); msg = (TextView) findViewById(R.id.msg); getPhoneInfo.setOnClickListener(this); //初始化Loading progressDialog = new ProgressDialog(this); progressDialog.setMessage("Loading"); //初始化代理对象 phonePresenter = new PhonePresenterImpl(this, this); } @Override public void toast(String msg, int requestTag) { } @Override public void showProgress(int requestTag) { if (null != progressDialog && !progressDialog.isShowing()) { progressDialog.show(); } } @Override public void hideProgress(int requestTag) { if (null != progressDialog && progressDialog.isShowing()) { progressDialog.dismiss(); } } @Override public void loadDataSuccess(PhoneNumInfo phoneNumInfo, int requestTag) { msg.setText(phoneNumInfo.toString()); } @Override public void loadDataError(Throwable e, int requestTag) { msg.setText(e.getMessage()); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.getphoneinfo: phonePresenter.getPhoneNumInfo(phoneNum.getText().toString(), REQUESTMSG); break; } } }
-
效果图
-
成功
-
失败
-
到这里,一个基于MVP的DEMO就写完了,其中有用到RxJava和Retrofit,但是没有做明确的说明,如果想了解可以阅读:RxJava 与 Retrofit 结合的最佳实践
源码下载地址戳这里
2017年2月19日,去掉冗余代码,简化了流程。
码字不易,感谢您的点赞!关注!评论!!!