Android Architecture Blueprints 学习之 TODO-MVP(二)

上一篇博客分析了 TODO-MVP 的主界面 TasksActivity 的 MVP 架构的 V 和 P,在本篇中来研究 M 这一层。

在对 Model 层的分析中,主要考虑两个因素:

  • 对外:P 需要什么功能?M 对外提供什么接口?
  • 对内:APP 的数据存储机制。

其中,第一点术语 MVP 架构的范畴,是这个系列主要研究的内容;第二点术语具体的技术实施方案,虽然与 MVP 没什么关系,也是值得学习的。

在下文中分别来研究。

M 的接口

TODO-MVP 中代表 M 的类是 TasksRepository,数据的存取都由它负责,它向 P 提供了一系列接口,供对方调用。

首先要考虑的问题是:M 向 P 提供的接口怎么设计呢?要解决这个问题,首先要思考 TODO-MVP 这个 Todo list 应用本身,对数据有哪些操作:

  • 获取一个 todo item
  • 获取所有 todo items
  • 保存一个 todo item
  • 完成、取消完成
  • 删除一个
  • 清空所有

说白了操作的对象就是 Todo Item,做上增删改查就行了。

TasksRepository 的实现思路仍然是将上面的思考总结到一个接口中,即 TasksDataSource,之后 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);
}

APP 的数据存储机制

TODO-MVP 的数据存储机制虽然跟 MVP 架构没关系,但也很有学习价值。它采用了一种内存-数据库-网络的三层结构,结构图见下图的左半边:

三层数据存储方式的特点:

  • 内存 cache - 快
  • 磁盘 (SQLiteDb) - 慢
  • 网络 - 最慢

想要在这三层间保持好同步关系是很难的,这里采用了一种简单的技术方案:

对于获取操作:

  • 先看内存 cache 中有没有
  • 再看本地数据库户中有没有
  • 最后只能联网获取数据了

对于写操作:

  • 先更新 Cache
  • 再更新本地数据库
  • 再更新服务器

TasksRepository

TasksRepository 是一个单例,它包含有三个成员:

private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;

Map<String, Task> mCachedTasks;
boolean mCacheIsDirty = false;

其中 mCachedTasks 就是内存中的 cache。mCacheIsDirty 表示 cache 是否过期,APP 在每次更新操作的时候标志 cache 过期。

mTasksRemoteDataSource 和 mTasksLocalDataSource 的实例是在 TaskActivity 中动态注入的:

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

从中可以看出:

  • FakeTasksRemoteDataSource 和 TasksLocalDataSource 都是单例
  • FakeTasksRemoteDataSource 和 TasksLocalDataSource 也继承自 TasksDataSource。

TasksRepository 和 FakeTasksRemoteDataSource 和 TasksLocalDataSource 三者关系可以简单地用下图来表示:

untitled1379217329

下面来看几个接口的实现:

获取所有 getTasks

代码如下:

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

    // 如果 cache 不为空且没过期,就从里面拿
    // 这也暗示了 cache 中含有所有条目
    if (mCachedTasks != null && !mCacheIsDirty) {
        // 通过回调把 cache 中的数据传回去
        callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
        return;
    }

    // cache 过期
    if (mCacheIsDirty) {
        // cache 过期从网上拉数据
        getTasksFromRemoteDataSource(callback);
    } else { // cache 没过期,能走到这说明 cache 为空
        // 从数据库拉去数据
        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);
            }
        });
    }
}

这里的逻辑我感觉有点不合理:

  1. cache 是含有一次刷新的所有数据的,因此在只要它不过时这里一定会命中。执行刷新操作就表示cache 过时了。
  2. 如果 cache 过时,从网上拉数据下来。
  3. 最后一个 else 我觉得有点奇怪,按理说应该网络超时之后再从数据库里读数据。

不过本文主要考虑的是 MVP 架构,对这里的技术细节先一放。

这里应该首先注意的是接口与回调

从代码中可以看出,TasksRepository 和 FakeTasksRemoteDataSource 和 TasksLocalDataSource 获取所有 items 的接口都是一样的,回调也是一样的,还是嵌套使用的。

这实现了总的数据源与它底下的不同子来源的数据使用同一套接口,这样就很方便,也提高了维护性。

删除所有 deleteAllTasks

代码如下:

@Override
public void deleteAllTasks() {
    mTasksRemoteDataSource.deleteAllTasks();
    mTasksLocalDataSource.deleteAllTasks();

    if (mCachedTasks == null) {
        mCachedTasks = new LinkedHashMap<>();
    }
    mCachedTasks.clear();
}

由于删除任务不用做什么判断,因此代码很清晰。从这个实现中也能看出方法的名称都是一致的,使用起来很方便。

至此,在本文中分析了 Model 层的设计,重点围绕 M 的接口,对其内部的实现细节没有作过多的分析。最后总结一下本文的两个重点:

  • 将数据存储功能总结为用接口
  • 总数据源与子数据源均集成一套接口,调用方法实现一致。