Android官方MVP架构项目解析

作者:spiritTalk

地址:http://www.jianshu.com/p/389c9ae1a82c

声明:本文是spiritTalk原创,已获其授权发布,未经原作者允许请勿转载

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

项目说明

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

目前已经完成的示例有:

  • todo-mvp:mvp基础架构示例。
  • todo-mvp-loaders:基于todo-mvp,获取数据使用了Loaders。
  • todo-mvp-databinding:基于todo-mvp,使用了数据绑定组件。

仍在进展中的示例有:

  • dev-todo-mvp-contentproviders:基于todo-mvp-loaders, 使用了Content Providers。
  • dev-todo-mvp-clean:基于todo-mvp, 使用了Clean架构的概念。
  • dev-todo-mvp-dagger:基于todo-mvp,使用了Dagger2进行依赖注入。

这对于一直困惑于到底该用何种架构的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等。

原文发布于微信公众号 - Android先生(cyg_24kshign)

原文发表时间:2017-12-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android机动车

Retrofit实现带进度下载

Retrofit是目前最主流的网络框架了,它对网络请求几近完美的封装,大大降低了我们开发者的研发难度,缩短了研发周期。最近项目中遇到了下载视频和图片文件的需求(...

14220
来自专栏7号代码

Android网络与数据存储——File存储(实现SD卡文件浏览器)

AndroidManifest.xml中manifest标签下有一个属性android:installLocation,用于指定应用程序安装在什么地方,该属性有...

31630
来自专栏CSDN技术头条

内存泄漏的检测、解决、防止

引言 今天又是没什么事情,好,不多说,直接进入我们的主题吧。 今天说的是关于内存泄漏的检测与解决。这个问题想必对于初学者是个迷,也不知道从何出入手,那么今天这个...

343100
来自专栏郭霖

Android图片加载框架最全解析(七),实现带进度的Glide图片加载功能

我们的Glide系列文章终于要进入收尾篇了。从我开始写这个系列的第一篇文章时,我就知道这会是一个很长的系列,只是没有想到竟然会写这么久。 在前面的六篇文章中,我...

51750
来自专栏Youngxj

安卓的四大组件之Activity-活动

19450
来自专栏Android Note

一次代码,建立多次

16730
来自专栏程序员互动联盟

【专业技术】Android内存泄漏简介

存在问题: 不少人认为JAVA程序,因为有垃圾回收机制,应该没有内存泄露。 解决方案: 其实如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它...

30330
来自专栏酷玩时刻

统一管理项目中的接口回调

无论是 Java Web 开发还是 Android 开发我们都避免不了异步处理业务的逻辑。有异步的出现一定就有异步结果(接口)的回调。你之前是不是有为每一个异步...

10240
来自专栏码农笔录

Android全能开源项目xUtils3开发教程、简单封装

19820
来自专栏小鄧子的技术博客专栏

【译】Retrofit 2 - 如何从服务器下载文件

如果你在阅读本文前没有写过任何一行Retrofit请求代码,那么最好看一下前面几篇博客。对于很多Retrofit使用者来说:定义一个下载文件的请求与其他请求几乎...

24710

扫码关注云+社区

领取腾讯云代金券