Google TODO-MVP详解

简单介绍

在日常的开发当中,经常会遇到需求的变动。这个东西是真的难以避免的,所以对于产品的基础框架就比较重要了。Google大大在I/O大会上提出来了Android开发方式是属于MVP的,即Model+View+Presenter。

另外在GitHub上面放出来了一个样例,供开发者学习,这里可以看到整个项目的简介。

Google通过一个TODO类的APP讲解了如何使用一些第三方库,已经如何基于他们构建自己的APP的框架。

目前的状态

Stable samples

SampleDescriptiontodo‑mvpDemonstrates a basic Model‑View‑Presenter (MVP) architecture and provides a foundation on which the other samples are built. This sample also acts as a reference point for comparing and contrasting the other samples in this project.todo‑mvp‑loadersFetches data using the Loaders API.todo‑databindingUses the Data Binding Library.todo‑mvp‑cleanUses concepts from Clean Architecture.todo‑mvp‑daggerUses Dagger2 to add support for dependency injection.todo‑mvp‑contentprovidersBased on the todo-mvp-loaders sample, this version fetches data using the Loaders API, and also makes use of content providers.todo‑mvp‑rxjavaUses RxJava to implement concurrency, and abstract the data layer.todo‑mvvm‑databindingBased on the todo-databinding sample, this version incorporates the Model‑View‑ViewModel pattern.

Samples in progress

SampleDescriptiondev‑todo‑mvp‑tabletAdds a master and detail view for tablets.dev‑todo‑mvvm‑rxjavaBased on the todo-rxjava sample, this version incorporates the Model‑View‑ViewModel pattern.

可以看到上面的一些库在Android中都比较常用,这一次我们先从最基础的todo-mvp来看。

可以在上面的地址中获取,也可以直接在命令行中采用

git clone https://github.com/googlesamples/android-architecture.git

的方法获取。

需要切换到todo-mvp分支中

git checkout todo-mvp

在一般阅读代码的时候,我会采取新建一个read-code的分支,便于自己看到自己的改动。

git checkout -b read-code

准备工作已经OK,我们来看看该项目的模块结构。

模块结构

整体来看Google大法是通过实际的业务模块来划分包的,当然你要是通过UI、Util、Data来划分也一点没有问题,个人习惯而已。如下:

  • BasePresenter & BaseView是所有的P和V的基础接口,BasePresenter中有一个**start()方法,V可以通过改方法调用P中的业务逻辑。BaseView中有一个setPresenter()**方法,通过该方法,在P的构造函数中将V关联起来。
  • data中的数据源分为了远程TasksRemoteDataSource和本地TasksLocalDataSource
  • addedittask顾名思义,这个模块有两种状态(添加和修改)在这个包里分了四块
  • AddEditTaskContract即合约类,在其中有两个内部接口,分别定义了V所要控制的UI逻辑和P所控制的业务逻辑。
  • AddEditTaskPresenter则为实现了Contract内部接口的P,在其中有业务的逻辑。
  • AddEditTaskFragment则为实现了Contract内部接口的V,
    在其中有UI的部分操作。
  • 肯定有人就会有疑问了,为何上面MVP都已经有了,那AddEditTaskActivity的作用又是什么呢?我所理解的Activity应该属于一个容器,在这个容器中做了一部分简单的初始化UI操作,如控制ToolBar的样式、加载Fragment等。

代码分析

AddEditTaskActivity分析

先来看看AddEditTaskActivity,顾名思义有「添加任务」和「编辑任务」的功能。这个Activity不是我们所认为的标准意义上的View类,上面提到了,它所做的工作。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);

        // Set up the toolbar.
      ...
       // Add Fragment  
      ...
        // Create the presenter
        mAddEditTaskPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskFragment,
                shouldLoadDataFromRepo);
    }

这个Activity处理了部分UI初始化工作,同时,创建了Presenter和载入了Fragment,即通过构造函数将V和P做了联系。

这里需要注意的是,在构造函数的第二个参数tasksPepository是通过Injection注入的。很多新手看到这个,就会有疑问,怎么找不到这个类。其实是这样的,在文件目录中,我们看到了和app同级的目录,存在mock和prod。其次在app.gradle里面设置了productFlavors来控制发行的版本。因此我们去mock文件夹下就能找到Injection类了。

public class Injection {

    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(context));
    }
}

这里通过注入,写入了一个假的远程Task数据源,用来做测试。

AddEditTaskContract分析

这是一个合约类,内部有两个内部接口,分别继承了V和P的基类,同时拓展了部分方法。

public interface AddEditTaskContract {
    //View中实现了UI的调用逻辑
    interface View extends BaseView<Presenter> {

        void showEmptyTaskError();
        ...
    }

    interface Presenter extends BasePresenter {
        //Presenter中实现了业务逻辑
        void saveTask(String title, String description);
        ... 
      }
}

AddEditTaskFragment分析

这个Fragment实现了合约类中的View,所以通过覆写BaseView中的setPresenter(),将Presenter和View关联起来。

public void setPresenter(@NonNull AddEditTaskContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }

其次覆写了合约类中的UI逻辑,在此我就不详说了,比较简单,大家可以自己看看。

AddEditTaskPresenter分析

在这个Presenter中,通过构造函数将Presenter和View关联在一起。构造函数的调用发生在上面的Activity中。

public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mIsDataMissing = shouldLoadDataFromRepo;
        //将Presenter和View关联在一起
        mAddTaskView.setPresenter(this);
    }

这里将数据源tasksRepository传入到Presenter的构造函数中,可以使得Presenter在处理业务逻辑的时候,将数据教由给Model层处理。

这样的好处是显而易见的,彻底的将M-V-P分离开来,实现了解耦,也让整个程序的框架变得清楚明了起来。

TasksRepository分析

TasksRepository继承自TaskDataSourceTaskDataSource这个类中定义了两个回调接口,以及部分实现方法。

interface LoadTasksCallback {

        void onTasksLoaded(List<Task> tasks);

        void onDataNotAvailable();
    }

    interface GetTaskCallback {

        void onTaskLoaded(Task task);

        void onDataNotAvailable();
    }

而TaskRepository中首先定义了两个数据源,一个是本地数据源mTasksLocalDataSource,另一个是远程数据源mTasksRemoteDataSoure

下来我们重点看看获取任务的方法getTasks(LoadTasksCallbac callback)

public void getTasks(@NonNull final LoadTasksCallback callback) {
        checkNotNull(callback);

        // Respond immediately with cache if available and not dirty
        if (mCachedTasks != null && !mCacheIsDirty) {
            //TODO: What is Map's function values()
            callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            return;
        }

        if (mCacheIsDirty) {
            // If the cache is dirty we need to fetch new data from the network.
            getTasksFromRemoteDataSource(callback);
        } else {
            // Query the local storage if available. If not, query the network.
            mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
                @Override
                public void onTasksLoaded(List<Task> tasks) {
                    refreshCache(tasks);
                    callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
                }

                @Override
                public void onDataNotAvailable() {
                    getTasksFromRemoteDataSource(callback);
                }
            });
        }
    }

我们来看看上面的代码:

  • 从Cache里面读并且Cache中数据没有被污染,则直接返回
  • 若Cache数据已经被污染,则获取远程的数据源。否则,直接从本地数据库查询,若查询不到,则获取远程数据源

以上,可以看出来,TasksRepository很好的将两个数据源的调度策略对外隐藏了起来,使得调用者不用关心如何选择数据源调度

总结

再往下面就是一些业务实现逻辑上面的事情了,大家可以自己去看看。

总的来说,Google给了我们一个很好的范例,通过一个简单的APP,把MVP结构摆在了我们面前。当然你可以不认可它,而要将Activity去掉,那也是可以的。我到觉得现有的这种结构,更加方便的能看出来MVP各个角色之间的关系。

正如Google在项目介绍中说为何使用Fragment的那样:

Notice also in the following illustration that this version of the app uses fragments, and this is for two reasons:
•   The use of both activities and fragments allows for a better separation of concerns which compliments this implementation of MVP. In this version of the app, the Activity is the overall controller which creates and connects views and presenters.
•   The use of fragments supports tablet layouts or UI screens with multiple views.

使用activities和framgents使得MVP更好的分离开,Activity更多的是相当于一个全局的控制器,将Views和Presenters进行联系起来。

最后,让我们结合这张图片来回顾一下MVP在这个APP中的应用:

  • Activity作为一个控制类,将View和Presenter连接在一起
  • Presenter直接调用Model层的Repository获取数据
  • Repository中则对外封装了数据源的获取逻辑,通过回调返回给上层

写着写着就这么长了,下一篇分析下TODO-MVP-RxJava。