Life doesn’t give you what your want, it gives you what you DESERVE!

0x00 序言

Android Architecture Blueprints
Android Architecture Blueprints

Google 官方在 GitHub 推出的Android 架构 Demo项目非常值得学习,通过 TODO app 的例子,讲解各种应用架构模式如何在实现需求的基础上,达到可维护性、扩展性、可测试性的优秀设计。

本系列博客文章聚焦于以下几点:

  1. 简明扼要介绍各个架构的思想与基础模型
  2. 在该架构模式下如何对 TODO App 的各个模块进行划分,以及划分的依据
  3. 取一个页面为例,作出类图,解析代码

Talk is cheap, show me the code.

0x01 业务逻辑

建议 clone 代码后真机运行体验,用一张图概括如下。

页面流程图
页面流程图

Google 也给出了一个wiki 页面说明 TODO app 的特性。至于为什么选择这样一个应用作为 demo,官方给出的解释如下:

它必须足够简单,便于你很快地理解其功能;同时也要足够复杂,这样才能展现不同设计的决策以及提供测试场景。

0x02 todo-mvp

关于 MVP

MVP 脱胎于大名鼎鼎的 MVC(Model-View-Controller)。

mvc
mvc

但凡做软件开发的人应该对 MVC 都不感到陌生,它的基本思想是将数据、视图、控制逻辑拆分。这三部分可以自由进行替换,比如在不改变数据接口的情况下,替换数据来源;或者是调整 UI 显示,而无需改动业务逻辑。

MVC 是一个具有历史意义的架构模式,它对天马行空、一团乱麻的软件设计进行规约,使后续对程序的修改和扩展简化,并且使重用成为可能。但是,它并不是最适合 Android 开发的架构模式。

MVC 为什么不适合 Android 开发

MVC 的核心思想是解耦,单一职责。在 Android 开发中,不使用 MVC 的主要原因是 Activity 的职责太重,往往要同时承担 View 和 Controller 的工作,这会造成 Activity 类非常庞大,UI 代码和逻辑代码交织,耦合严重。

MVP 为什么适合 Android 开发

MVP 的架构如下图。

MVP
MVP

它单独抽出了 Presenter 对象,实现了业务逻辑,用于控制 View 的显示变化,以及与 Model 进行数据交互。Presenter 和 View 互相持有,Model 仅被 Presenter 持有。

在理想状态下,Presenter 和 Model 仅包含 Java 代码,不含 Android SDK 内容。MVP 架构解决了 MVC 中 Activity 职责过多的问题,将 UI 功能分配给 View 单独管理,View 的接口仅向 Presenter 开放,相比 MVC,更好地实现了职责单一、解耦的需求。

如何抽象业务场景

Model

设计从数据层开始,数据层要承担什么职责呢?其实就是CRUD

  • 保存新建的任务
  • 查询已创建任务
  • 更新任务状态(未完成 -> 已完成)
  • 删除任务

数据的持久化方式,数据操作结果采用同步还是异步,这些问题也是设计过程中要考虑的。

基本的任务对象:Task

首先设计一个Task.java类,表示任务对象,它是一个典型的 Java Bean。在这里我们将 Task 设计成一个不可变的对象。在一个任务从“未完成”变为“完成”状态时,并不是修改原对象,而是丢弃掉原对象,再以相同id重新创建一个完成状态的对象。这样做的好处是逻辑简单,不可变对象线程绝对安全。缺点是当对象创建、销毁成本高的时候,会产生性能损失。在实际业务中应当酌情选择重用或者舍弃。

1
2
3
4
5
6
7
public final class Task {
private final String mId;
private final String mTitle;
private final String mDescription;
private final boolean mCompleted;
...
}

Model 层对外暴露的接口:TasksDataSource

本着面向接口编程的原则,隐藏数据操作具体实现代码,暴露出管理任务的接口(包含回调)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}

这个接口里有两处与我的编程思路有出入。首先是加载任务回调,我认为可以仅保留复数任务的接口,对于加载单条任务的需求,返回一个长度为 1 的列表就可以了。其次,在命名上,我习惯于将同步接口以getXXX,异步接口以loadXXX来声明。

有了这个接口,就可以提供给任务详情、任务列表等页面使用。接下来创建一个接口实现类,这个类对外的职责是实现数据操作接口,对内的职责是隐藏两种具体的数据操作实现(内存缓存、数据库、网络)。

数据源共有三级缓存:

  • In-memory cache - Fast
  • Disk (SQLiteDb) - Slow
  • Network - Very slow
1
2
3
4
5
public class TasksRepository implements TasksDataSource {
private final TasksDatasource mTasksRemoteDataSource; // 网络数据源
private final TasksDatasource mTasksLocalDataSource; // 本地数据源
...
}

读写数据策略可以概括为“依次读,全部写”,括号里是我补充的内容:

  • In every getoperation:

    • Return cache if available, or
    • return local copy if it exists (and update cache) , or
    • return remote copy ( and update local copy & cache)
  • Every write/deleteoperation will simply:

    • Update cache
    • Update local
    • Update remote

具体的网络数据源、本地数据库设计为常见写法,不赘述。

Fragment 即是 View

处理所有 UI 变化事件,比如显示 loading、显示任务列表、显示任务详情、显示任务状态变更 SnackBar 等,值得注意的是页面跳转操作也属于 UI 变化,是在 Fragment 里面实现的。不妨思考一下为什么“跳转”不放在 Presenter 中进行?一个原因是 Presenter 与 Android SDK 无关,而跳转需要 Context 对象,违背了这一原则。比如从“任务列表”跳转到“任务详情”时:

1
2
3
4
5
6
// TasksFragment.java
@Override
public void showAddTask() {
Intent intent = new Intent(getContext(), AddEditTaskActivity.class);
startActivityForResult(intent, AddEditTaskActivity.REQUEST_ADD_TASK);
}

View 里面没有任何业务逻辑,主要处理以下事件:

  • 加载中、加载成功、失败页面展示
  • 展现 Toast、SnackBar 等提示
  • 处理按钮、菜单点击事件(通常是将其甩给 Presenter 处理)
  • 处理页面跳转
  • 声明 Adapter
  • onResume时调用 Presenter 的start方法,启动页面
  • 提供isActive方法,供 Presenter 在异步回调返回时判断页面是否存活

最后一点是容易漏掉的,如果异步请求回调时页面被销毁,会导致各种难以预料的问题。

1
2
3
@Override public boolean isActive() {
return isAdded();
}

Presenter - 业务逻辑载体

承担了业务逻辑实现的职责,有时业务逻辑过于复杂会导致 Presenter 类太大,此时要考虑拆分业务逻辑到单独的类中,就变成了mvp-clean架构,clean 架构中使用Use Case(用例)处理细分的业务逻辑。

Presenter 由 Activity 构造,并且提供方法给 Activity 以及 View(Fragment)使用。

Activity - 将一切粘合到一起

现在我们有了 Model、View、Presenter 各个部分,而 Android 应用的页面入口是 Activity 类,我们在 Activity 中创建 Fragment(View)及 Presenter,在这两者之间建立关联。同时还要处理一些必须放在 Activity 中进行的操作,比如onSaveInstanceStateonOptionsItemSelected 等等。

任务列表功能实现

用类图来表示 MVP 模式下的任务列表功能相关类:

mvp
mvp