一、什么是MVP?
在做大型软件项目的时候,架构师通常会慎重考虑使用某一个架构,这是至关重要的。在大型项目中,随着代码量越来越庞大,模块越来越错综复杂,如果没有一个好的架构,整个项目就像是一张蜘蛛网,没有人可以读懂,维护起来就非常困难,对于需求的变更与扩展就难于登天,甚至是毁灭性的。通过一个好的软件架构设计,可以使程序模块化,做到模块内部的高聚合和模块之间的低耦合。高内聚就使得程序在开发的过程中,开发人员只需要专注于一点,提高程序开发的效率。低耦合使得阅读与维护更加容易,也易于进行后续的测试以及定位问题,这就需要从整个项目的横向和纵向两个维度来划分层次。横向维度就是从整个软件层面上划分为多个层次,各个层次仅执行自己的职责;纵向维度是指按照业务模块来分隔,这样整个项目的层次结构就一目了然。MVP模式就是从横向维度上划分为三层,即Model层、View层和Presenter层。
View层:也就是视图层。View层的职责很单一,负责界面的展示,以及用户和界面的交互操作。
Model层:也就是数据层。Model层的职责也很简单,建立数据模型以及数据的读取操作。例如我们最常用的从数据库或网络上请求数据,把请求到的数据转换为数据模型,以及将数据保存至本地或者网络上。
Presenter层:它的职责比较重,负责业务逻辑的处理,它也是连接View层与Model层的桥梁。在MVP架构中,Model与View无法直接进行交互,Presenter就起到至关重要的作用,Presenter层从Model层拿到需要的数据后,进行一系列的逻辑处理,最后交给View层展示给用户。这样View与Model完全隔离,不存在耦合,它们就可以自由变化,不会影响到业务逻辑,同时把业务逻辑从View中抽离出来,使得对业务逻辑的单元测试更加容易实现,也更方便快捷。
二、MVP的五要素
在MVP模式中,通常包括五大要素:
1、View, 负责UI展示,与用户交互。
2、View interface, View实现的接口,Presenter通过此接口与View交互,依赖于接口编程,降低耦合。
3、Model,负责数据建模,检索及存储数据。
4、Model interface, Model实现的接口,Presenter通过此接口与Model交互,降低耦合。
5、Presenter,负责逻辑处理,是View与Model的交互桥梁。
三、MVP与MVC的差异
MVP是从MVC架构模式衍生出来的一种模式。MVC将系统分解为模型、视图、控制器三层,每一层都相对独立,职责单一,在实现过程中可以专注于自身的核心逻辑。
从上图可以看出:
1、MVP模式中,View与Model隔离,不能直接交互;在MVC模式中,View与Model直接交互,Model改变通知View更新。
2、在MVP模式中,View与Presenter通常是一对一的,对于复杂的View可以绑定多个Presenter来处理逻辑。在MVC模式中,一个Controller可以被多个View共享,Controller负责决定显示哪一个View.
MVP模式相比MVC模式有以下几个优点:
1、Model与View完全分离,可以修改视图而不影响模型。
2、一个Presenter可以用于多个View,而不需要改变Presenter逻辑,这样就可以在完全不改变Prsenter的情况下替换View。
3、Presenter与View的交互是通过接口来进行的,也就是说业务逻辑与UI解耦了,那么我们就可以很方便地对业务逻辑进行单元测试。
四、Android应用为什么使用MVP模式?
在Android应用中,Activity并不是一个标准的MVC模式中的Controller,它的职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿,这就给调试,测试和维护都带来了困难。开发软件项目,单元测试是非常重要的一环,但Android开发中对于逻辑代码的单元测试非常不方便,它与Activity的UI展示掺杂在一起,完全依赖于真机或模拟器上的展示效果,效率非常低下。如果使用MVP模式,这些问题就迎刃而解了。
五、Android中MVP模式的例子
我们来学习一下google官方给出的mvp示例项目。项目地址:https://github.com/googlesamples/android-architecture。
提供了六个基于MVP架构的小项目:
todo-mvp: 基础的MVP架构。
todo-mvp-loaders:基于MVP架构的实现,在获取数据的部分采用了loaders架构。
todo-mvp-databinding: 基于MVP架构的实现,采用了数据绑定组件。
todo-mvp-clean: 基于MVP架构的clean架构的实现。
todo-mvp-dagger2: 基于MVP架构,采用了依赖注入dagger2。
dev-todo-mvp-contentproviders: 基于mvp-loaders架构,使用了ContenPproviders。
我们来学习一个最简单的todo-mvp项目。
这个项目实现的功能是备忘录。首先看一下项目的整体结构,主要是按照功能模块来划分目录结构,主目录下有BaseView和BasePresenter两个接口,还有六个包,分别是任务的添加编辑(addedittask),任务完成情况的统计(statistics),任务的详情(taskdetail),任务列表的显示(tasks),数据存取(data)以及工具类(util)。先看一下两个基类接口BasePresenter和BaseView
public interface BasePresenter {
void start();
}
它是Presenter的基类,start方法的作用是从Model中获取数据,最终调用View接口显示。
public interface BaseView<T> {
void setPresenter(T presenter);
}
它是所有View的基类,setPresenter方法用于给View设置Presenter。
Model层的实现:
Model层在data包目录下,主要是对数据的存取操作。
public interface TasksDataSource {
interface LoadTasksCallback {
void onTasksLoaded(List<Task> tasks);
void onDataNotAvailable();
}
interface GetTaskCallback {
void onTaskLoaded(Task task);
void onDataNotAvailable();
}
void getTasks(@NonNull LoadTasksCallback callback);
void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
void saveTask(@NonNull Task task);
}
这是Model接口,Presenter通过此接口存取数据。LoadTasksCallback和GetTaskCallback定义了数据操作的回调接口,他们在Presenter里实现,当数据操作成功或失败后用于通知Presenter.
public class TasksLocalDataSource implements TasksDataSource {
@Override
public void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback) {
//获取taskId对应的task
if (task != null) {
callback.onTaskLoaded(task);
} else {
callback.onDataNotAvailable();
}
}
@Override
public void saveTask(@NonNull Task task) {
checkNotNull(task);
SQLiteDatabase db = mDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(TaskEntry.COLUMN_NAME_ENTRY_ID, task.getId());
values.put(TaskEntry.COLUMN_NAME_TITLE, task.getTitle());
values.put(TaskEntry.COLUMN_NAME_DESCRIPTION, task.getDescription());
values.put(TaskEntry.COLUMN_NAME_COMPLETED, task.isCompleted());
db.insert(TaskEntry.TABLE_NAME, null, values);
db.close();
}
}
TasksLocalDataSource是Model的具体实现。
Presenter层的实现:
首先给出一个契约类,定义了Presenter和View所有的接口,可以很清晰看到Presenter和View的所有操作,便于维护和管理。
public interface AddEditTaskContract {
interface View extends BaseView<Presenter> {
void showEmptyTaskError();
void showTasksList();
void setTitle(String title);
void setDescription(String description);
boolean isActive();
}
interface Presenter extends BasePresenter {
void createTask(String title, String description);
void updateTask( String title, String description);
void populateTask();
}
}
我们再看一下具体的Presenter类AddEditTaskPresenter
public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
TasksDataSource.GetTaskCallback {
public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
@NonNull AddEditTaskContract.View addTaskView) {
mTaskId = taskId;
mTasksRepository = checkNotNull(tasksRepository);
mAddTaskView = checkNotNull(addTaskView);
mAddTaskView.setPresenter(this);
}
@Override
public void start() {
if (mTaskId != null) {
populateTask();
}
}
@Override
public void populateTask() {
if (mTaskId == null) {
throw new RuntimeException("populateTask() was called but task is new.");
}
mTasksRepository.getTask(mTaskId, this);
}
@Override
public void onTaskLoaded(Task task) {
// The view may not be able to handle UI updates anymore
if (mAddTaskView.isActive()) {
mAddTaskView.setTitle(task.getTitle());
mAddTaskView.setDescription(task.getDescription());
}
}
}
首先将此Presenter设置给View,然后调用Model层的接口getTask方法,等待Model层执行回调函数onTaskLoaded,在回调函数里调用View层接口更新View。
View层的实现:
View是在Fragment中实现的,Activity作为Context,完成对Fragment的add和remove。
public class AddEditTaskFragment extends Fragment implements AddEditTaskContract.View {
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
@Override
public void setPresenter(@NonNull AddEditTaskContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}
@Override
public void setTitle(String title) {
mTitle.setText(title);
}
}
View很简单,就是更新UI,接收事件调用Presenter处理业务逻辑。
六、MVP单元测试
MVP各层的单元测试选型
P层:不需要任何Android环境,因此使用Junit测试即可。
V层:使用Google强大的Espresso进行UI的测试。测试过程中需要真机或模拟器,并做真实的操作。
M层:涉及到数据库相关操作,因此需要依赖Android环境,使用AndroidJUnitRunner进行测试
可以参考android官方MVP单元测试的用例。