Leo's Blog

MVC,MVP,MVVM 架构分析

MVC,MVP,MVVM 作为 Android 开发中耳熟能详的三个框架,一直处半懵逼的状态,最近分析了一些谷歌官方给出的samples代码,记录下自己的理解,轻喷~

MVC,MVP,MVVM

MVC

Model View Controller,软件中最常见的一种框架,Controller 层负责操作 Model 数据,并且返回给 View 层进行展示。

MVC 架构图

从上图看出,用户通过 view 层发出指令到 controller 层,controller 通知 model 层更新数据,model 完成后直接显示在 view 层上。

既然是 Android 猿,那我们从分析下 MVC模型在 Android 中的提现:

  • View : layout.xml
  • Model: 各种 JavaBean
  • Controller : Activity,Fragment 等等

我们来分析一个场景:点击按钮下载文件
按钮是卸载xml里面的,属于view层的
网络请求,数据解析相关代码写在其他类里,比如 netHelper ,属于model层。
view 和 model 通过button.setOnClickListener()关联其来,其卸载activity中,对应于 controller 层。

好像很清晰,看起来没毛病的样子~开始搞事情。。。
如果数据回来需要在view层进行展示,控制某些控件的隐藏/显示,xml文件就无能为力了,我们只能将代码写在 activity 中。activity 就开始找不到自己的归属了,我是谁? view or controller?
如果一个逻辑复杂的页面,维护起来简直是噩梦。

当然从上图也可以看到Model层和View层并没有隔离开,这是一个重要的缺陷。违背了程序的低耦合原则。

MVP

MVP 作为 MVC 的演化,解决了不少 MVC 的问题。对于 android 来说,MVP 的 model 和 MVC 是一样的,而 activity 和 fragment 不再是 controller 层,而是纯粹的 view 层,所有关于用户事件的转发全部交由 presenter 层处理。
MVP 架构示例图

优势

看图说话~我们来分析一下,MVP架构是如何解决 MVC 面对的问题:

  • view 层和 model 层完全解耦。
    Presenter 充当桥梁,view 层发出的事件传递到 presenter 层,由P层操作model,获取数据后通知 view 层更新UI。

  • activity 和 fragment 臃肿的问题。
    activity 和 fragment 属于 view 层,仅仅是数据回调后更新 UI。在activity中和fragment中没有任何与model相关的逻辑代码,而是将这部分代码放到presenter层中,通过接口的形式将 view 层需要的数据返回。

  • 便于测试。
    比如如果我们需要测试获取数据功能,只需要实现对应的接口,看presenter是否调用了相应的方法。也可以在presenter中制造假数据,分发给view ,用来测试布局是否正确。

  • 逻辑清晰,耦合性低,可维护性提高

缺陷

  • 接口过多,一定程度影响我们的编码效率和代码可读性
  • 逻辑比较复杂的页面,Activity 代码还是会比较多,当然要比MVC要好的多,而且逻辑清晰
  • Presenter 层比较臃肿

最佳实践

  • 使用 Fragment 作为 view 层,而 activity 则是一个用于创建 view(fragment)和 presenter 的控制器。
  • 根据业务需求抽取基类,定义接口,公共的逻辑尽量抽取,减少代码量。

MVVM

MVVM 最早是由微软提出,先上图:
MVVM 架构图
从 MVVM 架构图分析,MVVM 和 MVP 的区别貌似不大,presenter 换成了 viewmodel层,还有一点就是 view 层和 viewModel 层是binding的关系。viewmodel层的数据发生变化时,view层会相应的更新UI。

DataBinding

DataBinding框架
Android 平台 MVVM 的目前相当火爆,谷歌欧巴的DataBinding框架功不可没。它可以让我们轻松的实现MVVM。看了网上很多的文章,都说DataBinding就是ViewModel层,我有点不同的看法,在这里提出来,大家讨论一下。

  • 众多博文的观点: Android 中 MVVM 就是 Databinding 框架的运用。Android Data Binding中的 ViewModel是根据layout自动生成的Binding类,比如 activity_main.xml ,生成的Binding类就是ActivityMainBinding。

  • 不同观点:ViewModel 是可以进行 binding 的数据模型,Binding 类是View 和 ViewModel 之间的桥梁。ViewModel层数据对象三种形式:

  1. 继承BaseObservable
  2. Observable Fields
  3. Observable Collections
    这样定义的数据对象发生改变时,同步更新UI。
    DataBinding 框架通过setContentView(int resourceId )setXXX() 方法完成View和ViewModel的绑定。

小结:理论知识讲完了,我们总结一下

  • 了解区分 MVC,MVP,MVVM
  • 初步了解这三种模式在Android中的使用。
  • DataBinding 在 MVVM 模式中的职责

其实,真正的最佳实践都是人想出来的,我们并不一定要死磕一种模式。see一下谷歌大大实现的架构。

Android 官方架构分析

Android - architecture介绍

项目地址:android-architecture
Android框架提供了很大的灵活性去创建一个APP。这种灵活性很有价值,但是也让app有很多类,不一致的命名和不规范的框架结构。导致测试,维护,扩展困难。
Android Architecture 演示了帮助解决或避免这些问题的常用方案,该项目用不同的架构实现了同一个APP。当然这些samples仅仅是作为参考,在我们创建自己的app时,还是根据需求选择最合适的。

稳定的samples

屏幕快照 2017-03-10 15.00.21.png-98.7kB

正在开发的samples

屏幕快照 2017-03-10 15.00.41.png-47.6kB

废话不多说了,下面选择两个我想要在项目中使用的两个架构进行分析:
todo-mvptodo-mvp-databinding

TODO-MVP架构分析

todo-mvp 是基础的MVP架构,没有使用其他的任何类库。

app设计

该示例项目的代码组织方式完全按照功能组织

  • Tasks - 管理tasks列表
  • TaskDetail - 查看task详情,并提供删除功能
  • AddEditTask - 增加和编辑 tasks
  • Statistics - 查看最近的日程

每一个功能内部分为xActivity,xContract,xFragment,xPresenter四个类文件。

  • Activity 类,用于创建Fragment 和 Presenter
  • Contract 类,这个与之前见到的mvp实现都不同,该类用于统一管理 view 和 presenter 的所有接口,使得 view 和 presenter 中的功能非常清晰。
  • Fragment类,View 层 ,实现 view 接口
  • Presenter类,Contract类中相应 Presenter 接口的实现类

结构图如下:
屏幕快照 2017-03-10 15.29.23.png-25.1kB

代码分析

基类

我们先来看两个Base接口类, BasePresenter 和 BaseView

1
2
3
public interface BasePresenter{
void start();
}

BasePresenter中含有方法start(),该方法的作用是presenter开始获取数据并调用 view 中的方法更新UI。其调用时机是在 Fragment 类的 onResume() 方法中。

1
2
3
public interface BaseView<T>{
void setPresenter(T presenter);
}

BaseView 中含有方法 setPresenter,该方法作用是将presenter示例传入 view 中,调用时机是 Presenter 实现类的构造函数中。

契约类

之前说过契约类,是与之前所见的MVP实现不同,也是我感觉很优雅的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
view 和 presenter 的职责很明了,增删改都很方便
public interface TaskDetailContract {
interface View extends BaseView<Presenter> {
void setLoadingIndicator(boolean active);
void showMissingTask();
...
}
interface Presenter extends BasePresenter {
void editTask();
void deleteTask();
void completeTask();
void activateTask();
}
}

Activity 的作用

Activity 在项目中是一个控制者,负责创建 view 及 presenter 实例,并将二者进行关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TaskDetailFragment taskDetailFragment = (TaskDetailFragment) getSupportFragmentManager()
.findFragmentById(R.id.contentFrame);
if (taskDetailFragment == null) {
taskDetailFragment = TaskDetailFragment.newInstance(taskId);
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
taskDetailFragment, R.id.contentFrame);
}
// Create the presenter
new TaskDetailPresenter(
taskId,
Injection.provideTasksRepository(getApplicationContext()),
taskDetailFragment);

MVP 的实现与组织

  • Presenter 创建时将 Fragment 作为参数传入构造方法

    1
    2
    3
    4
    5
    6
    public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {
    mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
    mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
    // view 调用setPresenter方法,注入 presenter 示例
    mTasksView.setPresenter(this);
    }
  • Fragment onResume() 方法中调用 presenter.start()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class TaskDetailFragment extends Fragment implements TaskDetailContract.View {
    ...
    @Override
    public void onResume() {
    super.onResume();
    mPresenter.start();
    }
    @Override
    public void setPresenter(@NonNull TaskDetailContract.Presenter presenter) {
    mPresenter = checkNotNull(presenter);
    }
    ...

分析代码得出view 和 presenter 相互持有,并且通过构造函数或set方法进行依赖注入。view 处理用户与界面的交互,presenter与model层交互,处理数据回调的逻辑判断,如果需要更新UI,直接调用view的方法,实现了 view 和 presenter 的各司其职。

Model 层设计

Model 最大的特点就是被赋予了获取数据的职责。与我们平常Model只定义Bean对象不同,todo-mvp中,数据的获取,存储,数据状态变化都是model层的任务。Presenter 只需要调用该层的方法并传入回调。

总结:MVP遵循类单一职责的编程原则,但是代码量相对增加,而且view层的代码依然略显臃肿,xml文件作为View层的能力依然很弱,UI的更新依然需要咋Fragment中处理。

MVVM 与 MVP 相结合架构分析

该架构基于 todo-mvp 示例并且使用了 DataBinding 库来展示数据和绑定事件。
它并不是严格的遵守 MVVM 模型 或者 MVP 模型,它同时使用了 ViewModel 和 Presenter 。

app 设计

目录结构与todo-mvp一致。只是多了一个ViewModel。Statistics 和 tasks 模块实现ViewModel的方式并不一样,我们来逐个分析。

屏幕快照 2017-03-11 13.14.33.png-31.5kB

Statistics 模块

Statistics 模块
主要类:Activity ,Fragment ,ViewModel,Presenter

  • StatisticsActivity : 创建 Fragment,ViewModel , Presenter对象,管理注入。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    StatisticsFragment statisticsFragment = (StatisticsFragment) getSupportFragmentManager()
    .findFragmentById(R.id.contentFrame);
    if (statisticsFragment == null) {
    statisticsFragment = StatisticsFragment.newInstance();
    ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
    statisticsFragment, R.id.contentFrame);
    }
    StatisticsViewModel statisticsViewModel = new StatisticsViewModel(getApplicationContext());
    // 注入viewModel,用于binding至xml文件
    statisticsFragment.setViewModel(statisticsViewModel);
    // viewmodel 实现 view, 构造方法注入到presenter
    StatisticsPresenter statisticsPresenter = new StatisticsPresenter(
    Injection.provideTasksRepository(getApplicationContext()), statisticsViewModel);
    // Fragment 注入 presenter。
    statisticsFragment.setPresenter(statisticsPresenter);
  • StatisticsFragment : 调用presenter的方法获取数据,绑定 viewmodel 至xml

    1
    2
    3
    4
    5
    public void setPresenter(@NonNull StatisticsPresenter presenter){
    mPresenter = checkNotNull(presenter)
    }
    mViewDataBinding.setStats(mStatisticsViewModel)
1
2
3
4
5
6
7
8
9
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="stats"
type="com.example.android.architecture.blueprints.todoapp.statistics.StatisticsViewModel" />
</data>
...
</layout>
  • StatisticsViewModel : 提供get set方法,binding 到 xml 文件,自动更新UI

  • StatisticsPresenter : 与Model 层交互,获取数据后传递给viewmodel,更新UI

Tasks模块

Tasks 模块 MVP 结构的组织与实现与todo-mvp一致。
主要类: TasksActivity,TasksFragment,TasksPresenter,TasksItemActionHandler,TasksViewModel。

  • TasksActivity :管理,创建对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    TasksFragment tasksFragment =
    (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
    if (tasksFragment == null) {
    // Create the fragment
    tasksFragment = TasksFragment.newInstance();
    ActivityUtils.addFragmentToActivity(
    getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
    }
    // Create the presenter
    mTasksPresenter = new TasksPresenter(Injection.provideTasksRepository(
    getApplicationContext()), tasksFragment);
    // 创建时注入Presenter,是为了方法的重用。
    TasksViewModel tasksViewModel =
    new TasksViewModel(getApplicationContext(), mTasksPresenter);
    // 注入viewModel,通过Binding类与xml文件关联
    tasksFragment.setViewModel(tasksViewModel);
  • TasksFragment : View 层,持有ViewModel 和 Presenter。将ViewModel绑定至xml文件,根据数据自动更新UI,调用Presenter的方法获取或更新数据,处理用户和组件的交互。

    1
    2
    3
    4
    5
    TasksFragBinding tasksFragBinding = TasksFragBinding.inflate(inflater, container, false);
    // 绑定viewmodel 和 xml文件,自动更新UI。
    tasksFragBinding.setTasks(mTasksViewModel);
    // 将presenter与xml绑定,作为事件处理类,重用方法
    tasksFragBinding.setActionHandler(mPresenter);
  • TasksPresenter : 和 todo-mvp 的presenter 没什么区别,获取数据,然后调用view的方法,更新UI。

  • TasksViewModel : 继承BaseObservable,在与Xml文件绑定后,内容发生改变时刻自动更新UI。
    注意 @Bindable 注解和 notifyPropertyChanged()的使用;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    public class TasksViewModel extends BaseObservable {
    int mTaskListSize = 0;
    private final TasksContract.Presenter mPresenter;
    private Context mContext;
    public TasksViewModel(Context context, TasksContract.Presenter presenter) {
    mContext = context;
    mPresenter = presenter;
    }
    @Bindable
    public String getCurrentFilteringLabel() {
    switch (mPresenter.getFiltering()) {
    case ALL_TASKS:
    return mContext.getResources().getString(R.string.label_all);
    case ACTIVE_TASKS:
    return mContext.getResources().getString(R.string.label_active);
    case COMPLETED_TASKS:
    return mContext.getResources().getString(R.string.label_completed);
    }
    return null;
    }
    @Bindable
    public String getNoTasksLabel() {
    switch (mPresenter.getFiltering()) {
    case ALL_TASKS:
    return mContext.getResources().getString(R.string.no_tasks_all);
    case ACTIVE_TASKS:
    return mContext.getResources().getString(R.string.no_tasks_active);
    case COMPLETED_TASKS:
    return mContext.getResources().getString(R.string.no_tasks_completed);
    }
    return null;
    }
    @Bindable
    public Drawable getNoTaskIconRes() {
    switch (mPresenter.getFiltering()) {
    case ALL_TASKS:
    return mContext.getResources().getDrawable(R.drawable.ic_assignment_turned_in_24dp);
    case ACTIVE_TASKS:
    return mContext.getResources().getDrawable(R.drawable.ic_check_circle_24dp);
    case COMPLETED_TASKS:
    return mContext.getResources().getDrawable(R.drawable.ic_verified_user_24dp);
    }
    return null;
    }
    @Bindable
    public boolean getTasksAddViewVisible() {
    return mPresenter.getFiltering() == ALL_TASKS;
    }
    @Bindable
    public boolean isNotEmpty() {
    return mTaskListSize > 0;
    }
    public void setTaskListSize(int taskListSize) {
    mTaskListSize = taskListSize;
    notifyPropertyChanged(BR.noTaskIconRes);
    notifyPropertyChanged(BR.noTasksLabel);
    notifyPropertyChanged(BR.currentFilteringLabel);
    notifyPropertyChanged(BR.notEmpty);
    notifyPropertyChanged(BR.tasksAddViewVisible);
    }
    }
  • TasksItemActionHandler : 绑定至xml文件,通过set方法注入,传入Presenter,可以重用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class TasksItemActionHandler {
    private TasksContract.Presenter mListener;
    public TasksItemActionHandler(TasksContract.Presenter listener) {
    mListener = listener;
    }
    /**
    * Called by the Data Binding library when the checkbox is toggled.
    */
    public void completeChanged(Task task, boolean isChecked) {
    if (isChecked) {
    mListener.completeTask(task);
    } else {
    mListener.activateTask(task);
    }
    }
    /**
    * Called by the Data Binding library when the row is clicked.
    */
    public void taskClicked(Task task) {
    mListener.openTaskDetails(task);
    }
    }

总结:该架构节省去了findViewById的苦力活,通过Binding库和ViewModel层增强了XMl的功能。
相比MVP,view层中的代码减少,UI展式改变只需改变ViewModel层。
项目MVVM,业务逻辑代码放在了Presenter中,明确了各层的职责。
这也是我比较倾向在项目中使用的架构。

写在最后

作为程序猿,纸上来的终觉浅,既然觉得浅了,咱们就去玩一些深的,自己去实现以下MVC,MVP,MVVM。实践出真知,真正的理解这三种架构。敬请期待~