Google TODO-MVP详解

简单介绍

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

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

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

目前的状态

Stable samples

Sample Description
todo‑mvp Demonstrates 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‑loaders Fetches data using the Loaders API.
todo‑databinding Uses the Data Binding Library.
todo‑mvp‑clean Uses concepts from Clean Architecture.
todo‑mvp‑dagger Uses Dagger2 to add support for dependency injection.
todo‑mvp‑contentproviders Based on the todo-mvp-loaders sample, this version fetches data using the Loaders API, and also makes use of content providers.
todo‑mvp‑rxjava Uses RxJava to implement concurrency, and abstract the data layer.
todo‑mvvm‑databinding Based on the todo-databinding sample, this version incorporates the Model‑View‑ViewModel pattern.

Samples in progress

Sample Description
dev‑todo‑mvp‑tablet Adds a master and detail view for tablets.
dev‑todo‑mvvm‑rxjava Based 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中则对外封装了数据源的获取逻辑,通过回调返回给上层

我的小站欢迎过来逛逛。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容