Android MVP设计模式最佳实现

1.概述

MVP最大的优点就是易于维护,易于测试。MVP (Model View Presenter) 设计模式派生自MVC模式。不同点在于用presenter代替了controller。MVP设计模式是一套准则,依照这些准则可以写出可复用性高、可测性好的代码。. 根据MVP准则,应用可以分为三部分:Model、 View、Presenter。

  • MVP模式解决android的常见痛点:可维护性,可测试性问题。
  • Model View Presenter提高视图分离和方便单元测试。
  • 在MVP模式中,我们将后台任务从activity、fragment视图中分离出来,使它们独立于大多数与生命周期相关的事件。
  • 使用一致的架构和设计模式,开发过程就会变得更加一致和更加容易和透明。应用程序也会因此变得更加简单,应用整体可靠性会得到显著提高。应用程序代码也将变得更加干净和可读性更加强。代码维护也将更容易进行。

2.MVP的工作过程

Android MVP设计模式最佳实现_设计模式


MVP工作过程如上图所示。

MVP是用户界面软件架构模式,减少了UI组件的行为。这样,通过使用Presenter最大限度减少与Activity和Fragments的交互。Presenter只是一个控制器, 控制显示层的逻辑和更新相应的视图。我们可以在presenter层里写业务逻辑,这可以最小化与视图的交互,这使得测试更加容易和快速。因为我们不再需要测试输入和输入,我们不再需要设备或模拟器来输入相应的测试内容,我们可以直接在JVM运行测试用例。

3.MVP架构的层:Model、View、Presenter

3.1.Model

在干净的架构中,每个层都有自己的Model,并且Model仅包含与该层有关的数据。Model层是与数据密切相关的。如:

  • View有自己的ViewModel来在视图上展现数据。
  • Database Model可能需要检索数据库实体。
  • Network model可能需要展示从远程服务器检索回来的数据实体。
  • 当数据在两层之间移动时 – Model会从一层表示层传到另一层。

3.2.Presenter

Presenter是views和Model之间的中间人。同时,它也包含数据表示的业务逻辑。Presenter从Model获得数据,并在传给视图前格式化数据。当然,Presenter也会更新视图数据。

3.3.View

MVP设计模式中,Activity和Fragment和View都是被动的,它们不能够直接访问Model。view有Presenter的引用和将事件UI传递给Presenter。如onClick事件。Views 公开控制数据表示的方法来显示或隐藏特定元素。

4.Model、View、Presenter依赖关系

Android MVP设计模式最佳实现_ide_02


作为一个干净的架构中,一层必须依赖下一层。

  • View依赖presenter,presenter必须依赖use case
  • 代码不能跳过,如果View不能访问use case,presenter不能访问entity
  • 优秀的架构,它们的依赖都是向下依赖,在我看来。

5.MVP Demo

结构:

Android MVP设计模式最佳实现_设计模式_03


我们以一个登录页为例。为了使用我们的开发更一致。我们约定开发的目录组成原则:以一个模块(以Activity为单位),目录组成如下:

 
data
model
model
...
repo
repo
repo
...
Activity
ActivityMVP
ActivityPresenter
...

这样组织的理由:

  • 首先,如果我们的目录分得太细,那么无形将MVP的其中一个优势:易于维护,打回原形。所以我们通过减少目录的层级和约定类的命名的方式来达到我们的目的:容易看出MVP的结构和MVP准则的充分体现。
  • 其次,在一个干净的MVP架构中,每一层都有自己的Model,Model层是与数据密切相关的,我们用repo里的repository为model提供数据支持。所以我们将提供数据的类放在data目录下,model类和repository类分别放在model目录和repo目录下。
  • 在Model类后加后缀model,如:功能名+Model,这样命名的好处是此Model是提供什么功能数据的。
  • 在repository类后加repository,如:实体类+repository,这样的命令方式可以让我们马上知道这个 repository类是提供什么数据的。
  • 除了数据之外,presenter与view都放在一起,View就是Activity或fragment,所以View对应的activity和fragment就放在模块目录下,以了方便找到对应的presenter类,我们约定在presenter类后加后缀presenter,如:Activity类名(Fragment类)+Presenter,这样命名的好处,可以马上知道此Presenter是为谁服务的。
  • 在设计原则里,最佳实践要求我们面向接口编程,因此我们可以将Model、View、Presenter对应的接口,写在同一个接口中,接口命名约定以MVP结尾,如:Activity类(Fragment类)+MVP后缀,这样命名的好处是我们见名就知道此MVP接口是为那个View定义的。

例子如上图所示。下面开始我们的Demo演示,开发LoginActivity。

5.1. 界面

开发App我们都是从界面开始。打开activity_login.xml加上如下代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="@dimen/_8dp">


<EditText
android:id="@+id/loginActivity_firstName_editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="First Name"
tools:text="First Name" />

<EditText
android:id="@+id/loginActivity_lastName_editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Last Name"
android:layout_marginTop="@dimen/_8dp"
tools:text="Last Name" />

<Button
android:id="@+id/loginActivity_login_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:textColor="@android:color/white"
android:layout_marginTop="@dimen/_16dp"
android:text="Log in" />

</LinearLayout>

5.2.创建User实体类

这个类是用来存储我们的用户信息的。

package com.wong.mvpdemo.login.data.model;

public class User {
private int id;
private String firstName;
private String lastName;
public User(String firstName,String lastName){
this.firstName = firstName;
this.lastName = lastName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}
}

5.3.创建Model、View、Presenter接口LoginActivityMVP

根据我们的约定,此接口命名:Activity类(Fragment类)+MVP后缀,这
完成了界面和确定了我们的用户信息后,接下来我们就要规划一下MVP各层的职责(在接口层面来思考)。规划如下:

package com.wong.mvpdemo.login;

import com.wong.mvpdemo.login.data.model.User;

public interface LoginActivityMVP {
/**
* View接口
*/
interface View {
String getFirstName();

String getLastName();

void showInputError();

void showUserSavedMessage();

void setFirstName(String firstName);

void setLastName(String lastName);
}

/**
* Presenter接口
*/
interface Presenter {
void setView(View view);

void loginButtonClick();

void getCurrentUser();
}

/**
* Model接口
*/
interface Model {
void createUser(String firstName, String lastName);

User getUser();
}
}

5.4.实现Model、View、Presenter接口

5.4.1.实现Model接口

package com.wong.mvpdemo.login.data.model;

import com.wong.mvpdemo.login.LoginActivityMVP;
import com.wong.mvpdemo.login.data.repo.LoginRepository;

public class LoginModel implements LoginActivityMVP.Model {

private LoginRepository repository;

public LoginModel(LoginRepository repository) {
this.repository = repository;
}

@Override
public void createUser(String firstName, String lastName) {
if(repository == null) return;
repository.saveUser(new User(firstName,lastName));
}

@Override
public User getUser() {
if(repository != null) return repository.getUser();
return null;
}
}

我们LoginModel需要利用repository来获得用户数据,因此我们先定义respository的接口,再将其实现:
LoginRepository.java:

package com.wong.mvpdemo.login.data.repo;

import com.wong.mvpdemo.login.data.model.User;

public interface LoginRepository {
User getUser();
void saveUser(User user);
}

UserRepository.java:

package com.wong.mvpdemo.login.data.repo;

import com.wong.mvpdemo.login.data.model.User;

public class UserRepository implements LoginRepository {
private User user;
@Override
public User getUser() {
if(user == null){
user = new User("KingWill","Wong");
user.setId(0);
return user;
}
return user;
}

@Override
public void saveUser(User user) {
if(user == null){
user = getUser();
}
this.user = user;
}
}

5.4.2.实现Presenter接口

package com.wong.mvpdemo.login;

import android.text.TextUtils;

import androidx.annotation.Nullable;

import com.wong.mvpdemo.login.data.model.User;

public class LoginActivityPresenter implements LoginActivityMVP.Presenter {

@Nullable
private LoginActivityMVP.View view;
private LoginActivityMVP.Model model;

public LoginActivityPresenter(LoginActivityMVP.Model model){
this.model = model;
}

@Override
public void setView(LoginActivityMVP.View view) {
this.view = view;
}

@Override
public void loginButtonClick() {
if(view != null){
if(TextUtils.isEmpty(view.getFirstName().trim()) || TextUtils.isEmpty(view.getLastName().trim())){
view.showInputError();
}else{
model.createUser(view.getFirstName(),view.getLastName());
view.showUserSavedMessage();
}
}

}

@Override
public void getCurrentUser() {
User user = model.getUser();
if(user != null && view != null){
view.setFirstName(user.getFirstName());
view.setLastName(user.getLastName());
}
}
}

5.4.3.实现View接口

package com.wong.mvpdemo.login;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.wong.mvpdemo.R;
import com.wong.mvpdemo.login.data.model.LoginModel;
import com.wong.mvpdemo.login.data.repo.LoginRepository;
import com.wong.mvpdemo.login.data.repo.UserRepository;
import com.wong.mvpdemo.login.LoginActivityMVP;
import com.wong.mvpdemo.login.LoginActivityPresenter;

public class LoginActivity extends AppCompatActivity implements LoginActivityMVP.View{

LoginActivityMVP.Presenter presenter;

private EditText mETFirstName;
private EditText mETLastName;
private Button mBtnLogin;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
mETFirstName = findViewById(R.id.loginActivity_firstName_editText);
mETLastName = findViewById(R.id.loginActivity_lastName_editText);
mBtnLogin = findViewById(R.id.loginActivity_login_button);
LoginRepository repository = new UserRepository();
LoginActivityMVP.Model model = new LoginModel(repository);
presenter = new LoginActivityPresenter(model);
mBtnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.loginButtonClick();
}
});
}

@Override
protected void onResume() {
super.onResume();
presenter.setView(this);
presenter.getCurrentUser();
}

@Override
public String getFirstName() {
return mETFirstName.getText().toString();
}

@Override
public String getLastName() {
return mETLastName.getText().toString();
}

@Override
public void showInputError() {
Toast.makeText(this,"First Name or Last Name cannot be enmpty",Toast.LENGTH_LONG).show();
}

@Override
public void showUserSavedMessage() {
Toast.makeText(this,"User saved successfully",Toast.LENGTH_LONG).show();

}

@Override
public void setFirstName(String firstName) {
this.mETFirstName.setText(firstName);
}

@Override
public void setLastName(String lastName) {
this.mETLastName.setText(lastName);
}
}

至此Demo已写完,Demo下载地址:
​​​JAVA版本​​​​Kotlin版本​

6.总结

遵守MVP模式的准则来开发和组织代码可以使用可以方便维护和测试。使用MVP要注意目录的结构有保持一致,命名规范要一致,否则一切都是白搭,根本体现不了MVP的优势,反而让MVP成为累赘。