设计模式——Android(安卓)常用设计模式之MVP详解及项目实战
引言
Android经过这几年的不断发展壮大,APP的功能越来越强大,UI也越来越复杂,对于Android开发者来说UI层在程序开发过程中担任了越来越多的职责。通常一个APP是由多种数据模型(Model)和多种视图(View)组成,如果我们直接使用Model-View设计模型,那这将使得我们的程序代码变得复杂、耦合度高、不利于单元测试和代码重构。
一、MVP概述
MVP的全称为Model-View-Presenter,MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的人机交互都发生在Presenter内部,简而言之,View和Model层不直接交互,所有交互都是同P层来实现,在MVP模式下的Presenter要拥有“绝对权力”。如果没有它,
Model与View就是两个孤岛,尽管各有各的地盘(完全解耦),但不能发挥基本的作用。通过MVP中的View就真的代码精简了蛮多,View只要从相应的IView接口下实现相应的属性和一些简单方法就完事了,而最终调用IView接口下的那个视图实例则完全交给了Presenter。引用微软官方的一张图直观体会下MVP
二、MVP中四种重要角色及联系
虽然MVP的全称为Model-View-Presenter,看起来只有三种角色Mode、Presenter、View,实质上是四种角色——View、View interface、Model、Presenter。
1、View层
负责绘制UI元素,直接承担与用户进行交互的职能,在Android中通常体现为Activity、Fragment、View、Dialog等,在实际开发中也可以根据整个项目的业务对于所有View共性的可以抽象到一个基类。简单理解View 层只承担负责调用Presenter层职责
2、View Interface层
实质上是对View层的逻辑抽象,是需要View必须要实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试,在实际开发中也可以根据具体业务进行抽象到一个基类,一般具体的View interface 最好只对应一个View ,虽然理论上是可以对应多个View的,建议最好不要,于是比例应该是View:View Interface=1:1
3、Presenter层
实质上是作为View(实际上是View Interface)的逻辑代理实现(View层的具体逻辑实现应该放在Presenter层),自然是应该与View 一一对应,虽然我们从结构上能让一个Presenter为多个View提供逻辑支持。但是这样会导致Presenter里面的逻辑对到底实现哪个View产生一定的混乱,为了简单明了(个人观点)除了需要与View 交互,同时还需要去与Model交互,简而言之,Presenter层充当的的连接Model与View的桥梁作用,那么在代码上自然需要持有View Interface层和Model的引用,还是个人建议,为了简捷明了比例应该是Presenter:View Interface=1:1。
4、Model层
负责实现具体业务功能,Model层里才是功能的本质,其他三种角色最终通信的目的就是调用Model层的,主要负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合)等。这里值得再注明的是M不直接和Activity等直接关联,也不一定和V层、Presenter 层一一对应,一种比较好的习惯是按照功能模块来封装Model层,通常Model层除了真正的功能实现之外,还可以通过一些回调来反馈结果。结合以上的理解,完整的MVP模式比例应该是V:P:M=1:1:n(个人建议,仅供参考)总而言之,MVP最根本的目的就是使M和V层解耦,同时降低Activity、Fragment等View层代码的臃肿程度。
三、MVP模式的优点和不足
1、MVP模式的优点
有效降低了耦合度,实现了Model和View真正的完全分离
各功能模块职责划分明显,层次清晰
项目结构更加灵活,增强了代码可复用性
隐藏数据
降低耦合度,,可以修改View而不影响Modle
Model可以被复用
降低了单元测试的成本
View可以进行组件化。在MVP当中,View不直接依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务完全无知。它要做的只是调用P。这样就可以做到高度可复用的View组件。
把业务逻辑抽到Presenter中去,避免后台线程引用着Activity导致Activity的资源无法被系统回收从而引起内存泄露和OOM
2、MVP模式的不足
Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较臃肿,维护起来会比较麻烦。
额外的代码复杂度及学习成本
四、Android MVP实现的基本形式
1、定义View Interface 层
通常个人建议建议,可以把所有需要与P、V交互的操作抽象到View Interface 层。
ILoginView.java
package com.crazymo.mvpdemo.view.activity;/** * Auther: Crazy.Mo * DateTime: 2017/8/30 17:06 * Summary: */public interface ILoginView { void showLoading(); void hideLoading(); void toMainActivity(); void showUsernameErro();//仅仅在逻辑上去判断 void showPwdErro(); void onUsernameErro();//Model回调 void onLoginowPwdErro();}
2、实现View 层
通常View 层除了需要完成绘制界面的任务之外,还必须要承担另外两份职责——实现View Interface 层接口 和 持有对应的Presenter引用并初始化以及在不使用的时候释放引用,因为MVP模式与传统的M-V模式不同,V不直接和M交互,而是V通过P间接完成与M的交互,所以要想通过调用P就必须得持有对应的引用。
LoginActivity.java
package com.crazymo.mvpdemo.view.activity;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.ProgressBar;import com.crazymo.mvpdemo.R;import com.crazymo.mvpdemo.presenter.LoginPresenterImpl;import butterknife.Bind;import butterknife.ButterKnife;import butterknife.OnClick;public class LoginActivity extends AppCompatActivity implements ILoginView { @Bind(R.id.edt_user) EditText edtUser; @Bind(R.id.edt_pwd) EditText edtPwd; @Bind(R.id.progress_login) ProgressBar progressLogin; @Bind(R.id.btn_login) Button btnLogin; private LoginPresenterImpl presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); presenter=new LoginPresenterImpl(this); ButterKnife.bind(this); } @Override public void showLoading() { progressLogin.setVisibility(View.VISIBLE); } @Override public void hideLoading() { progressLogin.setVisibility(View.VISIBLE); } @Override public void toMainActivity() { MainActivity.showMainActivity(this); finish(); } @Override public void showUsernameErro() { edtUser.setError("Username can't be empty or Username is too short"); } @Override public void showPwdErro() { edtPwd.setError("Password can't be empty !"); } @Override public void onUsernameErro() { edtUser.setError("Username is not exits!"); } @Override public void onLoginowPwdErro() { edtPwd.setError("Username or password is erro!"); } @OnClick({R.id.btn_login}) void onClick(View view){ presenter.login(edtUser.getText().toString(),edtPwd.getText().toString()); } @Override protected void onDestroy() { super.onDestroy(); presenter.detachView(); }}
3、实现Presenter层
通常Presenter层作为M层和V层的桥梁,P层自然发挥承上启下的作用,所以P层的基本实现一般都会是持有对应的M并且初始化M,然后在V中调用P的时候实现在P内部通过M的引用调用M,这可以认为是启下的作用,而通常V层都需要根据M层的反馈结果做出不同的响应,所以还需要持有V的引用以及给M使用的回调。
ILoginPresenter.java
package com.crazymo.mvpdemo.presenter;/** * Auther: Crazy.Mo * DateTime: 2017/8/30 17:06 * Summary: */public interface ILoginPresenter { void login(String user, String pwd);//此例是以登录为例,而通常登录这个操作需要与M交互,因此可以把这个操作抽象到P层 void detachView();//这个是释放对应的view ,可以封装到一个基类中}
IOnLoginFinishListener.java
package com.crazymo.mvpdemo.presenter;/** * Auther: Crazy.Mo * DateTime: 2017/8/30 17:18 * Summary: */public interface IOnLoginFinishListener { void onLoginSuccess(); void onLoginFailed(String type);}
LoginPresenterImpl.java
package com.crazymo.mvpdemo.presenter;import com.crazymo.mvpdemo.model.ILoginModel;import com.crazymo.mvpdemo.model.LoginModelImpl;import com.crazymo.mvpdemo.view.activity.ILoginView;/** * Auther: Crazy.Mo * DateTime: 2017/8/30 18:03 * Summary: */public class LoginPresenterImpl implements ILoginPresenter ,IOnLoginFinishListener{ public static final String USER_NOT_EXITS = "user_not_exits"; public static final String USER_PWD_ERRO = "user_pwd_erro"; private ILoginView loginView;//P需要与V 交互,所以需要持有V的引用 private ILoginModel loginModel; public LoginPresenterImpl(ILoginView view) { this.loginView = view; this.loginModel = new LoginModelImpl(this); } @Override public void login(String user, String pwd) { if(loginView!=null) { loginView.showLoading(); if ("".equals(user)) { loginView.showUsernameErro(); loginView.hideLoading(); return; } if ("".equals(pwd)){ loginView.showPwdErro(); loginView.hideLoading(); return; } loginModel.login(user,pwd); } } @Override public void detachView() { //为了避免内存泄漏,在不用的时候及时释放所持有的View 引用 loginView=null; } @Override public void onLoginSuccess() { loginView.toMainActivity(); } @Override public void onLoginFailed(String type) { loginView.hideLoading(); if(USER_NOT_EXITS.equals(type)){ loginView.onUsernameErro(); }else if(USER_PWD_ERRO.equals(type)){ loginView.onLoginowPwdErro(); } }}
4、实现Model 层
Model 层作为真正的业务功能实现层,由于需要把结果反馈到V层,所以需要通过P层的回调来实现。
ILoginModel.java
package com.crazymo.mvpdemo.model;/** * Auther: Crazy.Mo * DateTime: 2017/8/30 16:56 * Summary: */public interface ILoginModel { void login(String user,String name);}
LoginModelImpl.java
package com.crazymo.mvpdemo.model;import android.util.Log;import com.crazymo.mvpdemo.presenter.IOnLoginFinishListener;import com.crazymo.mvpdemo.presenter.LoginPresenterImpl;/** * Auther: Crazy.Mo * DateTime: 2017/8/31 9:20 * Summary: */public class LoginModelImpl implements ILoginModel { public static final String DEFAULT_USER = "crazymo_"; public static final String DEFAULT_PWD = "cmo"; private IOnLoginFinishListener finishListener; public LoginModelImpl(IOnLoginFinishListener listener){ this.finishListener=listener; } @Override public void login(String user,String pwd) { int k=0; for(int i=0;i<100000;i++){ k++; } Log.e("CrazyMo_",Thread.currentThread().getName().toString()); if(DEFAULT_USER.equals(user)&& DEFAULT_PWD.equals(pwd)){ finishListener.onLoginSuccess(); }else if(!(DEFAULT_USER.equals(user))){ finishListener.onLoginFailed(LoginPresenterImpl.USER_NOT_EXITS); }else if(DEFAULT_USER.equals(user)&&!(DEFAULT_PWD.equals(pwd))){ finishListener.onLoginFailed(LoginPresenterImpl.USER_PWD_ERRO); } }}
五、Android MVP实现的结合泛型和弱引用的形式
1、View层(View Interface层代码不变参见源码)
package com.crazyview.mvppro.activity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.ProgressBar;import com.crazyview.mvppro.R;import com.crazyview.mvppro.presenter.LoginPresenterImpl;import butterknife.Bind;import butterknife.ButterKnife;import butterknife.OnClick;public class LoginActivity extends BaseActivity implements ILoginView { @Bind(R.id.edt_user) EditText edtUser; @Bind(R.id.edt_pwd) EditText edtPwd; @Bind(R.id.progress_login) ProgressBar progressLogin; @Bind(R.id.btn_login) Button btnLogin; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); ButterKnife.bind(this); } @Override public void showLoading() { progressLogin.setVisibility(View.VISIBLE); } @Override public void hideLoading() { progressLogin.setVisibility(View.VISIBLE); } @Override public void toMainActivity() { MainActivity.showMainActivity(this); finish(); } @Override public void showUsernameErro() { edtUser.setError("Username can't be empty or Username is too short"); } @Override public void showPwdErro() { edtPwd.setError("Password can't be empty !"); } @Override public void onUsernameErro() { edtUser.setError("Username is not exits!"); } @Override public void onLoginowPwdErro() { edtPwd.setError("Username or password is erro!"); } @OnClick({R.id.btn_login}) void onClick(View view){ presenter.login(edtUser.getText().toString(),edtPwd.getText().toString()); } @Override protected void onResume() { super.onResume(); presenter.attachView(this);//在Activity里的生命周期方法中完成Presenter和V的绑定并初始化 } @Override protected void onDestroy() { super.onDestroy(); } @Override public LoginPresenterImpl initPresenter() { return new LoginPresenterImpl(); }}
2、Presenter 层
封装了一个BasePresenter,同时把View的引用改为弱引用,在View 层的生命周期方法中完成Presenter的管理。
package com.crazyview.mvppro.presenter;import java.lang.ref.WeakReference;/** * Auther: Crazy.Mo * DateTime: 2017/9/7 17:10 * Summary: */public abstract class BasePresenter { protected WeakReference modelView; public void attachView(T view) { this.modelView = new WeakReference(view); } public void dettachView() { this.modelView.clear(); } protected T getView() { return modelView.get(); }}
继承BasePresenter实现具体的业务Presenter
package com.crazyview.mvppro.presenter;import com.crazyview.mvppro.activity.LoginActivity;import com.crazyview.mvppro.model.ILoginModel;import com.crazyview.mvppro.model.LoginModelImpl;/** * Auther: Crazy.Mo * DateTime: 2017/8/30 18:03 * Summary: */public class LoginPresenterImpl extends BasePresenter<LoginActivity> implements ILoginPresenter ,IOnLoginFinishListener{ public static final String USER_NOT_EXITS = "user_not_exits"; public static final String USER_PWD_ERRO = "user_pwd_erro"; //P需要与M 交互,所以需要持有M的引用 private ILoginModel loginModel; public LoginPresenterImpl() { this.loginModel = new LoginModelImpl(this); } @Override public void login(String user, String pwd) { if(getView()!=null) { getView().showLoading(); if ("".equals(user)) { getView().showUsernameErro(); getView().hideLoading(); return; } if ("".equals(pwd)){ getView().showPwdErro(); getView().hideLoading(); return; } loginModel.login(user,pwd); } } @Override public void onLoginSuccess() { getView().toMainActivity(); } @Override public void onLoginFailed(String type) { getView().hideLoading(); if(USER_NOT_EXITS.equals(type)){ getView().onUsernameErro(); }else if(USER_PWD_ERRO.equals(type)){ getView().onLoginowPwdErro(); } }}
3、Model层
package com.crazyview.mvppro.model;import android.util.Log;import com.crazyview.mvppro.presenter.IOnLoginFinishListener;import com.crazyview.mvppro.presenter.LoginPresenterImpl;/** * Auther: Crazy.Mo * DateTime: 2017/8/31 9:20 * Summary: */public class LoginModelImpl implements ILoginModel { public static final String DEFAULT_USER = "crazymo_"; public static final String DEFAULT_PWD = "cmo"; private IOnLoginFinishListener finishListener; public LoginModelImpl(IOnLoginFinishListener listener){ this.finishListener=listener; } @Override public void login(String user,String pwd) { int k=0; for(int i=0;i<100000;i++){ k++; } Log.e("CrazyMo_",Thread.currentThread().getName().toString()); if(DEFAULT_USER.equals(user)&& DEFAULT_PWD.equals(pwd)){ finishListener.onLoginSuccess(); }else if(!(DEFAULT_USER.equals(user))){ finishListener.onLoginFailed(LoginPresenterImpl.USER_NOT_EXITS); }else if(DEFAULT_USER.equals(user)&&!(DEFAULT_PWD.equals(pwd))){ finishListener.onLoginFailed(LoginPresenterImpl.USER_PWD_ERRO); } }}
完整源码传送门
小结
MVP当然还有其他的实现形式,为了适配更多类型的View还可以考虑泛型等,如果你真正的理解面向接口编程,灵活使用好抽象和接口,你也可以改造出更高级的MVP模式,比如说Google 官方正在维护的Android 架构等其他第三方的项目。以上只是个人对于MVP使用的一些经验总结和感悟。
更多相关文章
- 没有一行代码,「2020 新冠肺炎记忆」这个项目却登上了 GitHub 中
- No.11 使用firewall配置的防火墙策略的生效模式
- 如何在android上 使用gif图片(android开源库android-gif-drawabl)
- Android设计模式系列--原型模式
- Android(安卓)UI基础——EditText控件
- 用git的windows客户端msysgit下载android代码
- Android(安卓)Jamendo开源在线音乐播放器源码分析一 jamendo初步
- android校园二手市场客户端+服务端源代码
- Android设计模式系列(7)--SDK源码之命令模式