Android官方MVP架构项目解析

前段时间Google在Github开源了一个项目Android Architecture Blueprints,在项目中展示了使用不同的实现架构和工具来实现相同的app。

先来看项目说明:

项目目的是通过展示各种架构app的不同方式来帮助开发者解决架构问题。项目中通过不同的架构概念及方式实现了功能相同的app。你可以用示例来当做参考,或是干脆拿来当做创建app项目的基础。项目中,希望大家能把关注点集中到代码结构、整体架构、可测试性、可维护性这四个方面。当然实现app有很多种方式,千万不要把它当做定式。

目前已经完成的示例有:

仍在进展中的示例有:

这对于一直困惑于到底该用何种架构的android开发者来说是好事,开发者只要根据自己项目的情况,在不同的实现中进行选择(app规模、团队状况、维护工作量的大小、平板是否支持、代码简洁程度偏好等等,这些都会影响你的选择),重点是代码结构,整体架构、可测试性和可维护性。

** 本文是基于todo-mvp项目的分析。**

再来看App结构:

app主要包括以下四个界面(功能):


代办事项(列表&详情)界面
代办事项(新建&统计)界面

分别对应着代码的包结构,也就是说src目录的代码组织方式是按照功能来组织的,包中又分为Activity、Fragment、Contract、Presenter四种类文件。

androidTest(UI层测试)、androidTestMock(UI层测试mock数据支持)、test(业务层单元测试)、mock(业务层单元测试mock数据支持):

总的来说,app界面、功能代码结构以及测试代码结构非常清晰。

源码解析

1、首先来看两个Base接口类,BaseView与BasePresenter,两类分别是所有View和Presenter的基类。

public interface BaseView<T> {
    // 规定View必须要实现setPresenter方法,则View中保持对Presenter的引用。
    void setPresenter(T presenter);
}

setPresenter的调用时机是presenter实现类的构造函数中,如此View中的事件请求便通过调用presenter来实现。

public interface BasePresenter {
    // 规定Presenter必须要实现start方法。
    void start();
}

该方法的作用是Presenter开始获取数据并调用View的方法来刷新界面,其调用时机是在Fragment类的onResume方法中。

2、定义了契约类(接口)。
使用契约类来统一管理view与presenter的所有的接口,这种方式使得view与presenter中有哪些功能,一目了然,维护起来也很方便。以下通过详情界面(功能)来分析:

/**
 * This specifies the contract between the view and the presenter.
 */
public interface TaskDetailContract {

    interface View extends BaseView<Presenter> {
        // 设置数据加载状态
        void setLoadingIndicator(boolean active);
        // 处理task加载失败的情况
        void showMissingTask();
        // 隐藏待办事项title
        void hideTitle();
        // 显示待办事项title
        void showTitle(String title);
        // 隐藏待办事项的描述
        void hideDescription();
        // 显示待办事项的描述
        void showDescription(String description);
        ……
    }

    interface Presenter extends BasePresenter {
        // 修改待办事项
        void editTask();
        // 删除待办事项
        void deleteTask();
        // 标记完成 
        void completeTask();
        // 标记未完成
        void activateTask();
    }
}
  • TaskDetailContract中的View接口定义了该界面(功能)中所有的UI状态情况,TaskDetailFragment作为View层,实现了该接口,如此 TaskDetailFragment 只关注UI相关的状态更新,所有事件操作都调用 TaskDetailPresenter 来完成。
  • Presenter 接口则定义了该界面(功能)中所有的用户操作事件,TaskDetailPresenter 作为Presenter层,实现了该接口,如此 TaskDetailPresenter 则只关注业务层的逻辑相关,UI的更新只需调用View的状态方法。

3、Activity在mvp中的作用。
Activity在项目中是一个全局的控制者,负责创建view以及presenter实例,并将二者联系起来。TaskDetailActivity 的onCreate()回调中创建TaskDetailPresenter 实例,TaskDetailPresenter 的构造函数中实现了View和Presenter的关联。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ……
        // Create the presenter
        new TaskDetailPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                taskDetailFragment);
    }

    public TaskDetailPresenter(@Nullable String taskId,
                               @NonNull TasksRepository tasksRepository,
                               @NonNull TaskDetailContract.View taskDetailView) {
        this.mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!");
        // 保持对View(TaskDetailFragment)的引用
        mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!");

        // 使View(TaskDetailFragment)也保持对自身(TaskDetailPresenter)的引用
        mTaskDetailView.setPresenter(this);
    }

4、Model层。
该项目中Model层最大的特点是被赋予了数据获取的职责,与我们平常Model层只定义实体对象截然不同。实例中,数据的获取、存储、数据状态变化都是Model层的任务,Presenter会根据需要调用该层的数据处理逻辑并在需要时将回调传入。

我们来看TaskDetailPresenter 的 start() 方法:

    @Override
    public void start() {
        openTask();
    }

    private void openTask() {
        // 判空处理
        if (null == mTaskId || mTaskId.isEmpty()) {
            mTaskDetailView.showMissingTask();
            return;
        }
        // 更新状态
        mTaskDetailView.setLoadingIndicator(true);
        // 获取该条Task数据
        mTasksRepository.getTask(mTaskId, new TasksDataSource.GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // The view may not be able to handle UI updates anymore
                // View已经被用户回退
                if (!mTaskDetailView.isActive()) {
                    return;
                }
                // 获取到task数据,并更新UI
                mTaskDetailView.setLoadingIndicator(false);
                if (null == task) {
                    mTaskDetailView.showMissingTask();
                } else {
                    showTask(task);
                }
            }

            @Override
            public void onDataNotAvailable() {
                // The view may not be able to handle UI updates anymore
                // 显示数据获取失败时的状态
                if (!mTaskDetailView.isActive()) {
                    return;
                }
                mTaskDetailView.showMissingTask();
            }
        });
    }

我们接着看 TasksRepository 中的getTask() 方法,

    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        // 判空处理
        checkNotNull(taskId);
        checkNotNull(callback);

        // 获取缓存数据
        Task cachedTask = getTaskWithId(taskId);
        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }

        // Load from server/persisted if needed.

        // Is the task in the local data source? If not, query the network.
        // 从本地数据源(SQLite数据库)中获取
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // 成功,则回调
                callback.onTaskLoaded(task);
            }

            @Override
            public void onDataNotAvailable() {
                // 失败,则从远程数据源(网络)中获取
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // 回调成功时的方法
                        callback.onTaskLoaded(task);
                    }

                    @Override
                    public void onDataNotAvailable() {
                        // 回调失败时的方法
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

我们发现 TasksRepository 维护了两个数据源,一个是本地(SQLite数据库),一个是远程(网络服务器)。

    private final TasksDataSource mTasksRemoteDataSource;

    private final TasksDataSource mTasksLocalDataSource;

我们发现他们(包括TasksRepository类)都实现了 TasksDataSource 接口:

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);

    void completeTask(@NonNull Task task);

    void completeTask(@NonNull String taskId);

    void activateTask(@NonNull Task task);

    void activateTask(@NonNull String taskId);

    void clearCompletedTasks();

    void refreshTasks();

    void deleteAllTasks();

    void deleteTask(@NonNull String taskId);
}

这样一来我们就很容易扩展新的数据源(获取数据的方式),毕竟我们在TaskDetailActivity中初始化TasksRepository就是调用的如下方法,其实我们很容易把FakeTasksRemoteDataSource替换为TasksRemoteDataSource,把TasksLocalDataSource 替换为TasksContentProviderDataSource,这就是针对接口编程的好处吧。

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

最后,我们再来看这张图。Fragment作为View,View和Presenter通过Activity来进行关联,Presenter对数据的调用是通过TasksRepository来完成的,而TasksRepository维护着它自己的数据源和实现。

OK,通过以上的分析,我们可以看到:

  • 工程的整体架构和代码结构非常清晰(不再是所有的业务和逻辑都糅合在Activity、Fragment里了),易于理解和上手。
  • 由于将UI代码与业务代码进行了拆分,整体的可测试性非常的好,UI层和业务层可以分别进行单元测试。
  • 由于架构的引入,虽然代码量有了一定的上升,但是由于界限非常清晰,各个类和层的职责都非常明确且单一,后期的扩展,维护都会更加容易。

但以上毕竟是架构的Sample,是为了说明架构思想,因此有些地方我们在实际运用中需要注意:数据库和网络的操作都应该放在工作线程,用户回退后需要取消网络请求、回调接口置为null等。


转载请标明出处:http://www.jianshu.com/p/389c9ae1a82c

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

推荐阅读更多精彩内容