对Android-MVP架构模式的理解与初尝试

常,如果你是一名面向对象的开发者,或多或少都了解和接触过大名鼎鼎的“MVC”模式。

到了Android移动端上,因为其自身的某些特性。于是,从“MVC”模式里又衍生出了一种新的模式,既“MVP”模式。

 

关于其二者的特点,从根本来说,十分相似:

 

  • Controller/Presenter负责接受数据,并命令View或Model做出相应的修改;
  • Model负责封装应用程序的数据模型及业务逻辑。
  • View负责应用程序的显示。
而二者之间的最大的区别在于:
  • MVP:View并不直接使用Model,它们之间的通信是通过Presenter(MVC中的Controller)来进行的,所有的交互都发生Presenter内部。
  • MVC:允许View跳过Controller,直接对Model进行访问。

 

 

那么,到了Android开发当中,为什么MVP模式会显得比MVC模式更加合适,我们加以分析。

 

以传统的JavaWeb开发而言,其对应于MVC模式来说,通常表现为:

 

  • Servlet或者Filter作为Controller;
  • JSP作为View;
  • POJO + 封装BIZ的Action类作为Model。

 

 

我们按照同样的思想换算到Android当中,则应该表现为:

 

  • Activity作为Controller;
  • 布局xml文件作为View;
  • POJO + BIZ作为Model。

 

 

这时,细心的人可能已经注意到了,强行把Activity作为Controller似乎并不那么合理。

因为在Android当中:Activity除了负责接收用户的输入之外,还承担着加载界面布局,实例化控件,绑定监听事件等等工作。

 

你之所以选择架构模式,根本就是为了最大程度的对代码结构进行解耦和模块分离。

所以,如果出现Activity既像View,又像Controller这种不伦不类的情况,我们自然应该避免其发生。

 

也正是基于此,所以在衍生出的MVP模式中,选择这样做。

 

  • 将Activity作为View,只负责界面显示和用户交互。
  • Model依然保持本色。
  • 而建立Presenter负责View与Model的交互。

 

 

这个周末抽空花了一点时间,尝试了一下所谓的MVP模式。多多少少有些体会,在此记录。

 

首先,既然要使用到MVP模式,自然要了解为什么使用它。如果没有好处,我们用它搞毛呢?

简单归纳,其好处为:

 

  • 易于维护
  • 易于测试
  • 松耦合度
  • 复用性高
  • 健壮稳定

个人尝试用这种模式搭建项目结构,发现的确逻辑条理和项目结构更加清晰,会使得后期更容易进行维护工作。

不过相对于平常来说,经分离之后,类文件,接口文件反而相对却变多了。

所以个人感觉如果是规模较小,逻辑较为简单的项目,其实反而没有使用的必要。

 

话入正题,所谓的M-V-P,对应 来说:

M - Model数据模型(业务逻辑层)

既是主要处理伴随数据模型的业务逻辑层。

 

V - View 视图(表现层)

更基本的来说,也就是界面,负责显示数据及与用户进行交互。

 

P - Presenter 表示器。

个人认为可以理解为一个中转站,也就是M与P的连接枢纽。

其行为表现为:接收“V”上反馈的指令,分发给“M”进行处理,最后将处理的结果再反馈在“V”上。

 

知道了较为书面化的概念,结合一点生活中熟悉的事物,希望能帮助我们更好的进行理解。

我们这样想象,你开了一家小餐馆,那么对应来说:

V - 餐馆门市。

既消费者双眼所见的,门市既是整个“餐馆系统”的视图。

 

M - 餐馆厨房。

用户在“V”里,既是在“餐馆”内产生了交互行为“点餐”,“厨房”负责处理该业务逻辑,最终产生要反馈给用户的结果“菜肴”。

 

P - 餐馆店小二。

通常来说,食客来到餐馆,不会直接开始寻找厨房,然后直接告诉厨师自己所点的菜肴。而厨师也不会在完成菜品之后,再亲自上菜。

所以,你需要一个“店小二”,既MVP里的“P”。这里你就能十分形象的理解到“表示器P”其充当的角色,既一个“枢纽”的角色。

 

分析一下:

 

  1. 食客点餐完毕。
  2. 将菜单交至店小二;
  3. 店小二将菜单送到厨房;
  4. 厨房根据菜单做好菜肴,通知店小二;
  5. 店小二将菜肴上至对应餐桌。

 

对应来说:

 

  1. 用户在android设备上(既“V”)进行了操作;
  2. “V”接受到交互,将用户的行为传递给表示器“P”;
  3. 表示器收到反馈行为,通知对应的业务逻辑处理器;
  4. “M”收到,进行处理,将处理得到的结果返回给“P”;
  5. “P”得到结果,将结果返回给“V”,即反馈给用户;
 
这样加以对应,是否会对于理解MVP模式有所帮助?
 
 
话已至此,对于所谓的MVP,我们基本已经有了一定的了解,接着,就根据一个简单的例子,来加深自己的印象和理解。
以我在另一篇描述MVP模式的博客里看到的需求为例,假设我们现在有一个“验证登录”的需求,要实现以下效果:
 
 
接着,开始着手搭建项目,编写代码。
 
1、餐厅开业之前,我们要先搞定门市。OK,所以第一步,我们完成登录界面的布局文件的编写。
 
2、门市搞定了,为了生意,接着我们进行“装修”。没问题,那我们接着进行View的编写工作,完成装修(加载布局,实例化控件,绑定监听事件)。于是这时的LoginActivity应该是这样的:
[java] view plain copy
 
  1. public class LoginActivity extends Activity implements ILoginView {  
  2.     private EditText userName, passWord;  
  3.     private Button login, clearInput;  
  4.     private ProgressBar loginProgress;  
  5.   
  6.     @Override  
  7.     protected void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         setContentView(R.layout.activity_login);  
  10.         // Init view  
  11.         initView();  
  12.   
  13.     }  
  14.   
  15.     private void initView() {  
  16.     userName = (EditText) this.findViewById(R.id.user_name);  
  17.     passWord = (EditText) this.findViewById(R.id.password);  
  18.   
  19.     login = (Button) this.findViewById(R.id.btn_login);  
  20.     clearInput = (Button) this.findViewById(R.id.btn_clear);  
  21.   
  22.     login.setOnClickListener(new View.OnClickListener() {  
  23.   
  24.         @Override  
  25.         public void onClick(View v) {  
  26.             //need to do..  
  27.         }  
  28.     });  
  29.   
  30.     clearInput.setOnClickListener(new View.OnClickListener() {  
  31.   
  32.         @Override  
  33.         public void onClick(View v) {  
  34.             //need to do..  
  35.         }  
  36.     });  
  37.       
  38.     loginProgress = (ProgressBar) this.findViewById(R.id.login_loading);  
  39. }  
  40.   
  41.     }  
  42. }  

3.门市的装修工作终于完成了,我们送了一口气。这个时候,为了将来餐馆能够有条不紊的顺利营业。此时,也许我们就应该开始着手考虑,当我们的餐馆正式开始营业后,可能与消费产生哪些交互行为?从而定下一些好的营业行为规范了!
  
  对应于程序中来说,既是我们该View中需要与用户发生交互的一切行为。所以,对应我们“验证登录”的需求来说,接口的定义可能就类似于:
[java] view plain copy
 
  1. public interface ILoginView {  
  2.   
  3.     // 获取用户输入用户名  
  4.     String getUserNameInput();  
  5.   
  6.     // 获取用户输入密码  
  7.     String getPassWordInput();  
  8.   
  9.     // 登录成功,跳转界面  
  10.     void toAppMainActivity();  
  11.   
  12.     // 登录失败,提示用户  
  13.     void showLoginFailedInfo();  
  14.   
  15.     // 清空输入记录  
  16.     void clearInputValue();  
  17.       
  18.     // 提示正在登录  
  19.     void showLoginLoading();  
  20.       
  21.     // 隐藏正在登录提示  
  22.     void hideLoginLoading();  
  23. }  
 
4、规范我们已经定下了,通知小秘,去把经营规范打印出来,给我挂在餐馆里最显眼的地方!
所以我们此时要做的也是一样,既然View的接口已经声明完毕,此时我们就应该让Activity去实现接口,去落实老板定下的规范。
所以此时的Activity类经过完善,变成了下面这样:
[java] view plain copy
 
  1. public class LoginActivity extends Activity implements ILoginView {  
  2.   
  3.     private EditText userName, passWord;  
  4.     private Button login, clearInput;  
  5.     private ProgressBar loginProgress;  
  6.   
  7.     @Override  
  8.     protected void onCreate(Bundle savedInstanceState) {  
  9.         super.onCreate(savedInstanceState);  
  10.         setContentView(R.layout.activity_login);  
  11.         // Init view  
  12.         initView();  
  13.   
  14.     }  
  15.   
  16.     private void initView() {  
  17.         userName = (EditText) this.findViewById(R.id.user_name);  
  18.         passWord = (EditText) this.findViewById(R.id.password);  
  19.   
  20.         login = (Button) this.findViewById(R.id.btn_login);  
  21.         clearInput = (Button) this.findViewById(R.id.btn_clear);  
  22.   
  23.         login.setOnClickListener(new View.OnClickListener() {  
  24.   
  25.             @Override  
  26.             public void onClick(View v) {  
  27.                 // need to do..  
  28.             }  
  29.         });  
  30.   
  31.         clearInput.setOnClickListener(new View.OnClickListener() {  
  32.   
  33.             @Override  
  34.             public void onClick(View v) {  
  35.                 // need to do..  
  36.             }  
  37.         });  
  38.           
  39.         loginProgress = (ProgressBar) this.findViewById(R.id.login_loading);  
  40.     }  
  41.   
  42.     // implements interface method  
  43.     @Override  
  44.     public String getUserNameInput() {  
  45.         return userName.getEditableText().toString();  
  46.     }  
  47.   
  48.     @Override  
  49.     public String getPassWordInput() {  
  50.         return passWord.getEditableText().toString();  
  51.     }  
  52.   
  53.     @Override  
  54.     public void toAppMainActivity() {  
  55.         Intent intent = new Intent(LoginActivity.this, MainActivity.class);  
  56.         startActivity(intent);  
  57.   
  58.     }  
  59.   
  60.     @Override  
  61.     public void showLoginFailedInfo() {  
  62.         Toast.makeText(this, "Sorry,Plase check your input!", Toast.LENGTH_LONG).show();  
  63.     }  
  64.   
  65.     @Override  
  66.     public void clearInputValue() {  
  67.         userName.setText("");  
  68.         passWord.setText("");  
  69.     }  
  70.   
  71.     @Override  
  72.     public void showLoginLoading() {  
  73.         loginProgress.setVisibility(View.VISIBLE);  
  74.     }  
  75.   
  76.     @Override  
  77.     public void hideLoginLoading() {  
  78.         loginProgress.setVisibility(View.INVISIBLE);  
  79.     }  
  80. }  

5、这个时候对于店铺的打理已经基本进行完毕了,这个时候我们应该着手考虑一下餐厅的业务逻辑了,例如最关键的:找个好厨师~
没问题,也就是说此时我们应该开始着手于Model层的打架工作了。
在这里,对应于验证登录的业务需求,我们首先建立我们的数据模型:
[java] view plain copy
 
  1. public class User {  
  2.   
  3.     private String userName;  
  4.     private String passWord;  
  5.   
  6.     public String getUserName() {  
  7.         return userName;  
  8.     }  
  9.   
  10.     public void setUserName(String userName) {  
  11.         this.userName = userName;  
  12.     }  
  13.   
  14.     public String getPassWord() {  
  15.         return passWord;  
  16.     }  
  17.   
  18.     public void setPassWord(String passWord) {  
  19.         this.passWord = passWord;  
  20.     }  
  21.   
  22. }  
数据模型搞定之后,也就该完成针对于此数据模型进行处理的业务逻辑类了。在我们的例子中,只存在一个业务逻辑,既是验证登录。首先,我们依旧是首先定义下规范:
[java] view plain copy
 
  1. public interface ILoginModel {  
  2.   
  3.     void doLogin(String userName, String passWord, ILoginListener loginListener);  
  4. }  
接着,根据规范去定义具体的处理办法:
[java] view plain copy
 
  1. public class LoginModel implements ILoginModel {  
  2.   
  3.     @Override  
  4.     public void doLogin(final String userName, final String passWord, final ILoginListener loginListener) {  
  5.         new Thread(new Runnable() {  
  6.   
  7.             @Override  
  8.             public void run() {  
  9.                 // 模拟耗时操作  
  10.                 try {  
  11.                     Thread.sleep(2000);  
  12.                 } catch (InterruptedException e) {  
  13.                     // TODO Auto-generated catch block  
  14.                     e.printStackTrace();  
  15.                 }  
  16.                 // 验证登录  
  17.                 if (userName.equals("tsr") && passWord.equals("123")) {  
  18.                     User user = new User();  
  19.                     user.setUserName(userName);  
  20.                     user.setPassWord(passWord);  
  21.                     loginListener.loginSuccess(user);  
  22.                 } else {  
  23.                     loginListener.loginFailed();  
  24.                 }  
  25.   
  26.             }  
  27.         }).start();  
  28.     }  
  29.   
  30. }  
一切工作都在有条不紊的进行当中,唯一值得注意的是,在此我们定义了一个监听接口,用以监听登录结果,并根据结果做出对应操作。
[java] view plain copy
 
  1. public interface ILoginListener {  
  2.   
  3.     void loginSuccess(User user);  
  4.   
  5.     void loginFailed();  
  6. }  

6、当我们工作至此处时,厨房部门已经被我们打点完毕了。此时,万事俱备,只欠东风。我们只差一个关键的“店小二”了。
是的,当View层和Model层的搭建工作都已经完成,我们就只差一个Presenter来联系它们了。
 
你可以试着这样考虑,既然Presenter将作为View和Model的链接枢纽,那么肯定它是能够同时访问二者的。
这对应于程序来说,也就是,在Presenter类里,将必然存在View和Model的一个实例,从而你才能够调用他们的方法,完成交互。
 
而同时,就好比你作为餐馆老板招聘一名店小二,你肯定会明确的做出要求:既店小二开始上班以后,他的工作内容是什么。
所以在Presenter类里,你还应该根据实际,定义相应的行为。
就如同,餐馆老板在餐厅营业应为里规定了有“点餐”这个营业行为,那么当有顾客进行了“点餐”这个行为时,就应该调用“店小二”执行“收、取菜单”的动作。
对应我们本例当中,既是当有用户点击了“登录”按钮时,View就知道调用Presenter里的对应方法,再由Presenter去调用model里对应的方法。
 
所以,我们最终的Presenter类的定义可能如下:
[java] view plain copy
 
  1. public class LoginPresenter {  
  2.   
  3.     private ILoginModel loginModel;  
  4.     private ILoginView loginView;  
  5.     private Handler mHandler;  
  6.   
  7.     public LoginPresenter(ILoginView loginView) {  
  8.         this.loginModel = new LoginModel();  
  9.         this.loginView = loginView;  
  10.         mHandler = new Handler();  
  11.     }  
  12.   
  13.     public void doLogin() {  
  14.         loginView.showLoginLoading();  
  15.           
  16.         loginModel.doLogin(loginView.getUserNameInput(), loginView.getPassWordInput(), new ILoginListener() {  
  17.   
  18.             @Override  
  19.             public void loginSuccess(User user) {  
  20.                 mHandler.post(new Runnable() {  
  21.   
  22.                     @Override  
  23.                     public void run() {  
  24.                         loginView.hideLoginLoading();  
  25.                         loginView.toAppMainActivity();  
  26.   
  27.                     }  
  28.                 });  
  29.   
  30.             }  
  31.   
  32.             @Override  
  33.             public void loginFailed() {  
  34.                 mHandler.post(new Runnable() {  
  35.   
  36.                     @Override  
  37.                     public void run() {  
  38.                         loginView.hideLoginLoading();  
  39.                         loginView.showLoginFailedInfo();  
  40.   
  41.                     }  
  42.                 });  
  43.   
  44.             }  
  45.         });  
  46.     }  
  47.   
  48.     public void clearInputValue() {  
  49.         loginView.clearInputValue();  
  50.     }  
  51. }  

7、到了这里你的餐厅完全已经做好开始营业的准备了!唯一的工作,记得为你的“店小二”办理入职手续。
对应到我们的程序来说,既是将Presenter的实例添加到View当中,并且在各个按钮的监听事件里,安排Presenter进行对应的行为。
于是,最终的Activity变成了:
[java] view plain copy
 
  1. public class LoginActivity extends Activity implements ILoginView {  
  2.   
  3.     private LoginPresenter mPresenter;  
  4.     private EditText userName, passWord;  
  5.     private Button login, clearInput;  
  6.     private ProgressBar loginProgress;  
  7.   
  8.     @Override  
  9.     protected void onCreate(Bundle savedInstanceState) {  
  10.         super.onCreate(savedInstanceState);  
  11.         setContentView(R.layout.activity_login);  
  12.         // Init presenter  
  13.         mPresenter = new LoginPresenter(this);  
  14.         // Init view  
  15.         initView();  
  16.   
  17.     }  
  18.   
  19.     private void initView() {  
  20.         userName = (EditText) this.findViewById(R.id.user_name);  
  21.         passWord = (EditText) this.findViewById(R.id.password);  
  22.   
  23.         login = (Button) this.findViewById(R.id.btn_login);  
  24.         clearInput = (Button) this.findViewById(R.id.btn_clear);  
  25.   
  26.         login.setOnClickListener(new View.OnClickListener() {  
  27.   
  28.             @Override  
  29.             public void onClick(View v) {  
  30.                 mPresenter.doLogin();  
  31.             }  
  32.         });  
  33.   
  34.         clearInput.setOnClickListener(new View.OnClickListener() {  
  35.   
  36.             @Override  
  37.             public void onClick(View v) {  
  38.                 mPresenter.clearInputValue();  
  39.             }  
  40.         });  
  41.           
  42.         loginProgress = (ProgressBar) this.findViewById(R.id.login_loading);  
  43.     }  
  44.   
  45.     // implements interface method  
  46.     @Override  
  47.     public String getUserNameInput() {  
  48.         return userName.getEditableText().toString();  
  49.     }  
  50.   
  51.     @Override  
  52.     public String getPassWordInput() {  
  53.         return passWord.getEditableText().toString();  
  54.     }  
  55.   
  56.     @Override  
  57.     public void toAppMainActivity() {  
  58.         Intent intent = new Intent(LoginActivity.this, MainActivity.class);  
  59.         startActivity(intent);  
  60.   
  61.     }  
  62.   
  63.     @Override  
  64.     public void showLoginFailedInfo() {  
  65.         Toast.makeText(this, "Sorry,Plase check your input!", Toast.LENGTH_LONG).show();  
  66.     }  
  67.   
  68.     @Override  
  69.     public void clearInputValue() {  
  70.         userName.setText("");  
  71.         passWord.setText("");  
  72.     }  
  73.   
  74.     @Override  
  75.     public void showLoginLoading() {  
  76.         loginProgress.setVisibility(View.VISIBLE);  
  77.     }  
  78.   
  79.     @Override  
  80.     public void hideLoginLoading() {  
  81.         loginProgress.setVisibility(View.INVISIBLE);  
  82.     }  
  83. }  

至此,我们已经完全采用M-V-P的架构模式来搭建完成了“登录验证”的小项目。
对此我们加以总结,其实可以得出:对于MVP模式的构建,其规律为:
  • Activity现在只编写最基本的功能代码,如:加载布局,实例化控件,绑定监听等。
  • 将与用户发生交互的界面操作,都抽象到View接口中,然后由Activity实现接口,进而具体实现。
  • 需要对用户的行为进行处理或反馈的方法(例如上面我们的代码中登录与清楚按钮的点击事件处理),都抽离出来放到Presenter当中。
  • 需要对针对于数据模型进行操作的方法(例如将用户输入的数据进行持久化存储或需要在服务器进行处理等)都放在业务逻辑类,即Model当中。
  • Presenter从View接收数据,调用Model进行处理;从Model获取处理的结果,调用View反馈到界面。
来看一下最终的代码结构变成了什么样,是不是条理有变得更加清晰:
 
posted @ 2017-05-02 18:06  天涯海角路  阅读(201)  评论(0)    收藏  举报