0%

Android MVP架构解析

前言

Android Architecture Blueprints 是谷歌官方开源的一组 Android 架构方案,项目以多个分支分别采用 MVPMVVM 的概念以及 dagger、rxjava、databinding、livedata 等工具库,演示了一个简单的 TodoApp 在不同架构模式下的代码组织方式。这个项目中的代码非常规范,架构模式也非常有借鉴意义,是一个很有价值的学习素材。

本文将通过 项目组织架构组织与通信设计原则 这三个层次对项目中的todo-mvp分支进行解析,从而达到学习的目的。

项目概览

在分析之前,我们先通过截图来了解一下TodoApp的功能。

TodoApp
用来跳转列表页与统计页的抽屉窗口 统计页
列表页
详情页 编辑页

知晓了项目功能后,再来看项目的组织。

项目组织

在开始之前推荐大家先了解一下IDEA符号表,熟悉符号表对理解项目组织有很大帮助。

总览

TodoMVP 项目目录
app/src 除了 androidTest、main、test 这三个常见的模板目录外,还有androidTestMock、mock与prod 目录。androidTestMock 虽不常见,但通过名称可推断它是一个测试相关的目录,另外还有 mock、prod 这两个目录,我们稍后会讲到。

UI模块划分

TodoMVP 项目目录
app/src/main/java 下的 addedittask、statistics、taskdetail、tasks 这四个目录分别对应 App 内编辑页、统计页、详情页、列表页这四个模块,每个目录都有自己的 Activity、Fragment与Presenter,分别表示 MVP 中的 View 层和 Presenter 层,Contract 由字面意义推断为契约接口,此处暂且不表,稍后会在源码中了解其实际内涵。

Model层

TodoMVP 项目目录
app/src/main/java 下的 data 目录表示 Model 层,Task 是经final修饰的实体类模型,data/source 下有表示数据源接口的 TasksDataSource 与表示数据仓库类的 TasksRepository,data/source/local 与 data/source/remote 分别表示本地数据源与远程数据源,由ToDoDatabase及TastsDao可推断其local部分以数据库的形式实现。

最下方的 BasePresenter、BaseView 分别表示Presenter与View的基类,剩下的utils包用到时再进行查阅即可。

小结

经过简单的查看,不难作出如下推论。

  1. 项目采用MVP模式进行代码组织,每个功能模块均有其专属的 Activity、Fragmeng 与Presenter,所有的功能都围绕 Task 进行 CURD,所以 Mode 层只有一个实体类。
  2. Model 层内涵了加载数据的业务逻辑
  3. Model 层采用了 local 和 remote 双数据源,推断 TasksRepository 拥有缓存策略。

同时还有一些疑问:

  1. M-V-P是怎样组织的?
  2. M-V-P间如何进行通信?

接下来便通过代码来找寻答案

架构组织与通信

架构组织

1. 从最简单的Base接口开始看

1
2
3
4
5
6
7
8
9
//BaseView.java
public interface BaseView<T> {
// 规定View必须实现setPresenter方法,因此View必然持有Presenter的引用
void setPresenter(T presenter);
}
//BasePresenter.java
public interface BasePresenter {
void start();
}

2. Tasks契约接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//TasksContract.java
public interface TasksContract {
interface View extends BaseView<Presenter> {
//设置加载指示器是否显示
void setLoadingIndicator(boolean active);
//显示任务列表
void showTasks(List<Task> tasks);
//进入新增任务页
void showAddTask();
//进入任务详情页
void showTaskDetailsUi(String taskId);
//显示任务被标记为完成状态的提示
void showTaskMarkedComplete();
//显示任务被标记为活跃状态的提示
void showTaskMarkedActive();
//显示完成状态的任务被清除的提示
void showCompletedTasksCleared();
//显示任务加载失败的提示
void showLoadingTasksError();
//显示无任务视图
void showNoTasks();
//显示无活跃任务视图
void showNoActiveTasks();
//显示无完成任务视图
void showNoCompletedTasks();
//修改顶部Label文字
void showActiveFilterLabel();
void showCompletedFilterLabel();
void showAllFilterLabel();
//显示成功保存信息的提示
void showSuccessfullySavedMessage();
//判断Fragment是否活跃
boolean isActive();
//显示过滤器菜单弹窗
void showFilteringPopUpMenu();
}
interface Presenter extends BasePresenter {
//Fragment.onActivityResults回调
void result(int requestCode, int resultCode);
//加载任务列表
void loadTasks(boolean forceUpdate);
//添加新任务
void addNewTask();
//打开任务详情
void openTaskDetails(@NonNull Task requestedTask);
//标记某任务已完成
void completeTask(@NonNull Task completedTask);
//标记某任务未完成
void activateTask(@NonNull Task activeTask);
//清除已完成任务
void clearCompletedTasks();
//设置过滤器
void setFiltering(TasksFilterType requestType);
//获取过滤器
TasksFilterType getFiltering();
}
}

TasksContract 采用内部接口的形式将 View 与 Presenter 结合在一处,使得 View 和 Presenter 中有哪些功能看起来一目了然,维护时也很方便。

  • TasksContract.View 声明了任务列表所有的UI动态
  • TasksContract.Presenter 声明了任务列表的所有业务事件

3. 契约接口的实现类

1
2
3
4
//TasksFragment.java
public class TasksFragment extends Fragment implements TasksContract.View {
//TasksPresenter.java
public class TasksPresenter implements TasksContract.Presenter {

TasksFragment 与 TasksPresenter 分别实现了 TasksContract.View 与 TasksContract.Presenter 接口。因此 TasksFragment 对应任务列表模块的 View 层,TasksPresenter 对应任务列表模块的 Presenter 层。

  • TasksFragment 负责 View 层的视图改动,当有用户事件发生或者需要获取数据时,需交由 Presenter 处理
  • TasksPresenter 负责 Presenter 层的事件处理

4. View 与 Presenter 如何建立联系

从TasksActivity入手

tasks_act.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//tasks_act.xml
<androidx.drawerlayout.widget.DrawerLayout>
<LinearLayout>
<com.google.android.material.appbar.AppBarLayout />
<androidx.coordinatorlayout.widget.CoordinatorLayout>
// Fragment 占位容器
<FrameLayout
android:id="@+id/contentFrame"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<com.google.android.material.floatingactionbutton.FloatingActionButton />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
<com.google.android.material.navigation.NavigationView />
</androidx.drawerlayout.widget.DrawerLayout>

TasksActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//TasksActivity.java
protected void onCreate(Bundle savedInstanceState) {
//...
TasksFragment tasksFragment =
(TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (tasksFragment == null) {
tasksFragment = TasksFragment.newInstance();
// 将TasksFragment 关联到 TasksActivity
ActivityUtils.addFragmentToActivity(
getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
}

// 实例化Presenter时,将repository,fragment作为参数传入
mTasksPresenter = new TasksPresenter(Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
//...
}

在TasksActivity的onCreate方法内attach了TasksFragment,同时对TasksPresenter进行实例化,而TasksPresenter的构造函数与tasksFragment有很强的关联。

追踪 TasksPresenter 的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//TasksPresenter.java
public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
//采用断言检测参数是否为Null
mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
//为View设置Presenter
mTasksView.setPresenter(this);
}
//BasePresenter 基类继承而来的方法,一旦被调用便执行 loadTasks
@Override
public void start() {
loadTasks(false);
}

//TasksFragment.java
@Override
public void onResume() {
super.onResume();
//当 Fragment 的生命周期到达 resume 状态时,调用 presenter 的 start 方法
mPresenter.start();
}

@Override
public void setPresenter(@NonNull TasksContract.Presenter presenter) {
//采用断言检测参数是否为Null,若非Null则将其赋值给 mPresenter
mPresenter = checkNotNull(presenter);
}

在TasksPresenter的构造方法中,终于看到 Presenter 层与 View 层是如何关联的,至此,MVP之间的桥梁总算架通,接下来我们将视线转移到层次间的交互上。

组件通信:列表刷新的前世今生

刷新事件发生时,TasksFragment 将其转交给 TasksPresenter 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//TasksFragment.java

// 用户点击菜单控件,Fragment收到点击事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_clear:
mPresenter.clearCompletedTasks();
break;
case R.id.menu_filter:
showFilteringPopUpMenu();
break;
case R.id.menu_refresh:
//若用户点击的是刷新按钮,则交由presenter 的 loadTasks 方法进行处理
mPresenter.loadTasks(true);
break;
}
return true;
}

TasksPresenter 对 loadTastk(true) 的响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//TasksPrenseter.java

@Override
public void loadTasks(boolean forceUpdate) {
// loadTasks(true) 时,下条语句可直接看做 loadTasks(true,true);
loadTasks(forceUpdate || mFirstLoad, true);
mFirstLoad = false;
}

private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
if (showLoadingUI) {
//显示加载中动画
mTasksView.setLoadingIndicator(true);
}
if (forceUpdate) {
//强制更新时,将repo的脏缓存标志设置为true,达到放弃缓存的目的
mTasksRepository.mCacheIsDirty = true;
}

mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
//数据源成功回调
@Override
public void onTasksLoaded(List<Task> tasks) {
List<Task> tasksToShow = new ArrayList<Task>();

for (Task task : tasks) {
switch (mCurrentFiltering) {
case ALL_TASKS:
tasksToShow.add(task);
break;
case ACTIVE_TASKS:
if (task.isActive()) {
tasksToShow.add(task);
}
break;
case COMPLETED_TASKS:
if (task.isCompleted()) {
tasksToShow.add(task);
}
break;
default:
tasksToShow.add(task);
break;
}
}
// 如果 TasksFragment 不活跃,则中止操作,直接返回
if (!mTasksView.isActive()) { return; }
// 关闭加载动画
if (showLoadingUI) { mTasksView.setLoadingIndicator(false); }
if (tasks.isEmpty()) {
// 当列表为空时,根据过滤器状态显示对应的空列表提示信息
switch (mCurrentFiltering) {
case ACTIVE_TASKS:
//将View层的变动交由mTasksView即TasksFragment处理
mTasksView.showNoActiveTasks();
break;
case COMPLETED_TASKS:
mTasksView.showNoCompletedTasks();
break;
default:
mTasksView.showNoTasks();
break;
}
} else {
// 列表不为空时,调用mTasksView.showTasks来展示最新获取到的任务列表
mTasksView.showTasks(tasks);
showFilterLabel();
}
}
//数据源失败回调
@Override
public void onDataNotAvailable() {

if (!mTasksView.isActive()) {
return;
}
//显示加载失败提示
mTasksView.showLoadingTasksError();
}
});
}

TasksPresenter通过TasksRepository获取数据,无论成功或失败,都会通过mTasksView即TasksFragment来完成View的更新。

TasksFragment 对 showTasks 的响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//TasksFragment.java
@Override
public void showTasks(List<Task> tasks) {
//通知adapter
mListAdapter.replaceData(tasks);
//显示任务列表
mTasksView.setVisibility(View.VISIBLE);
//隐藏无任务视图
mNoTasksView.setVisibility(View.GONE);
}
//TasksFragment.TasksAdapter
public void replaceData(List<Task> tasks) {
mTasks = checkNotNull(tasks);
//通知ListView更新
notifyDataSetChanged();
}

TasksRepository 对 getTasks 的响应

1. Repository 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//TasksDataSource.java
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);
}
//TasksRepository.java
public class TasksRepository implements TasksDataSource {
//单例模式
private static TasksRepository INSTANCE = null;
//持有本地数据源与远程数据源
private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;
//私有化构造方法,屏蔽 new 操作
private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
@NonNull TasksDataSource tasksLocalDataSource) {
mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
}
//单例方法
public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource, TasksDataSource tasksLocalDataSource) {
if (INSTANCE == null) {
INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
}
return INSTANCE;
}

2. Repository 内的缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//获取任务列表
@Override
public void getTasks(@NonNull final LoadTasksCallback callback) {
checkNotNull(callback);
// 如果缓存可用,则返回缓存内容
if (mCachedTasks != null && !mCacheIsDirty) {
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
return;
}
if (mCacheIsDirty) {
// 如果存在脏缓存,则重新从远程获取数据
getTasksFromRemoteDataSource(callback);
} else {
// 不存在脏缓存时,尝试从本地数据库内获取数据
mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
//若在本地数据库内成功获得数据,则将数据回调给 Presenter
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
//若在本地数据库获得失败,则重新从远程获取数据
@Override
public void onDataNotAvailable() {
getTasksFromRemoteDataSource(callback);
}
});
}
}
//从远程获取数据
private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
//若成功获得数据,则将数据回调给 Presenter
@Override
public void onTasksLoaded(List<Task> tasks) {
//刷新缓存标志
refreshCache(tasks);
//刷新本地数据库
refreshLocalDataSource(tasks);
//将数据回调给 Presenter
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
//若成功获得数据,则将将失败事件回调给 Presenter
@Override
public void onDataNotAvailable() {
callback.onDataNotAvailable();
}
});
}

组件作用

Contract 在 MVP 中的作用

Contract 是每个模块的契约,对模块内的 View 和 Presenter 起到提纲擎领的作用,是编写代码前首先需要思考的内容,它的职责如下:

  1. 整合 View 与 Presenter 接口,便于维护
  2. 为 View 与 Presenter 的功能与职责做出规范,便于阅读及理解
  3. 作为 View 和 Presenter 的抽象,是遵循依赖反转原则(Dependency Inversion Principle)的前提

Activity 在 MVP 中的作用

Activity 是每个模块的入口,它的职责如下:

  1. 响应 Actiivty 的生命周期事件
  2. 加载 layout ,响应来自 drawer 及 fab 的用户事件
  3. 实例化 Fragment(View) 及 Presenter,并建立二者间的关联
  4. 为 Presenter 注入 Repository

关于第4点,将在后边的依赖反转章节进行详细阐述。

Fragment 在 MVP 中的作用

Fragment 作为 Fragment 的子类及 Contract.View 接口的实现类,它有以下几点职责:

  1. 响应 Fragment 的生命周期事件
  2. 加载 layout ,完成子控件的实例化及映射,对用户事件进行处理或将其中转给 Contract.Presenter
  3. 作为 Contract.View 接口的具体实现,对 View 层进行更新

Presenter 在 MVP 中的作用

Presenter 仅作为 Contract.Presenter 接口的实现类,它有如下几点职责:

  1. 纯 Java 代码,与 Android SDK 完全解耦,为测试提供便利
  2. 响应 Fragment 中转来的用户事件,对用户事件进行处理或将其中转给 Repository
  3. 响应 Repository 的回调,随后将 Model 层返回的数据中转给 Contract.View

Repository 在 MVP 中的作用

Repository 作为 Model 模块内的组件以及 DataSource 接口的实现类,它的职责如下:

  1. Repository 本身并未存储数据,它仅负责数据的缓存策略
  2. 将来自 Presenter 的调用先进行 CURD 缓存策略的处理,再中转给实际的 LocalDataSource / RemoteDataSource,并将最终结果回调给 Presenter

MVP间的通信方式

小结

  1. 项目的模块划分与代码结构非常清晰,使得项目易于理解与上手
  2. Activity 与 Fragment 的同时使用,带来了更好的分离性,Acitivity 作为模块入口兼控制器,负责view、presenter的创建与连接
  3. UI代码与业务代码进行了拆分,整体的可测试性非常好,UI层和业务层均可单独进行测试。

设计模式

面向对象设计的 S.O.L.I.D 原则

缩写 名称 描述
SRP 单一职责原则(Singel responsibility principle) 每个类、接口尽可能的只负责单方面的工作
OCP 开闭原则(Open-closed princile) 对扩展开放,对修改关闭
LSP 里式替换原则(Liskov substitution princile) 程序中的对象可以在不改变其正确性的条件下替换为它的子类对象
ISP 接口隔离原则(Interface segregation princile) 尽量使用多个小而精接口代替一个大而多的超级接口,从而在接口改动时减少对上层调用者的影响
DIP 依赖反转原则(Dependency inversion princile) 因为里式替换原则的存在,鼓励对象间的引用尽可能的依赖抽象而不是细节(即尽可能的使用父类型作为对象的引用类型),以此来降低依赖时的耦合度

todo-mvp 中设计原则的体现

  1. View 与 Presenter 相互持有时,都使用父类对象作为引用,满足了里式替换原则和依赖反转原则
  2. Model、View 与 Presenter 间的划分很清晰,满足了接口隔离原则与单一职责原则
  3. Repository、LocalDataSource、RemoteDataSource 对 DataSource 的不同实现满足了开闭原则

一个依赖注入的细节

TasksActivity 的 onCreate 在创建 TasksPresenter 时,通过 Injection 来获取 Repository。Injection 通常伴随强烈的依赖注入语义,此处必有蹊跷

1
2
3
4
5
6
7
8
//TasksActivity.java onCreate
protected void onCreate(Bundle savedInstanceState) {
//...
mTasksPresenter = new TasksPresenter(
Injection.provideTasksRepository(getApplicationContext()),
tasksFragment);
//...
}

回到项目组织,在 mock 与 prod 下,果然看到两个同名的 Injection 类
TodoMVP 项目目录
除 RemoteDataSourcew 外,两份Injection的代码完全一致,甚至连包名都一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/mock 
package com.example.android.architecture.blueprints.todoapp;
public class Injection {

public static TasksRepository provideTasksRepository(@NonNull Context context) {
checkNotNull(context);
ToDoDatabase database = ToDoDatabase.getInstance(context);
// mock 环境使用 FakeTasksRemoteDataSource
return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
TasksLocalDataSource.getInstance(new AppExecutors(),
database.taskDao()));
}
}
// src/prod
package com.example.android.architecture.blueprints.todoapp;
public class Injection {

public static TasksRepository provideTasksRepository(@NonNull Context context) {
checkNotNull(context);
ToDoDatabase database = ToDoDatabase.getInstance(context);
// prod 环境使用 TasksRemoteDataSource
return TasksRepository.getInstance(TasksRemoteDataSource.getInstance(),
TasksLocalDataSource.getInstance(new AppExecutors(),
database.taskDao()));
}
}

回到项目组织,打开Build Variant窗口,同时打开app下的 build.gradle文件

TodoMVP 项目目录

根据 mock/java 的蓝色文件夹图标可知,mock 下的 Injection.java 为当前源码集的一部分,项目编译时运行时会选用这个 Injection

我们在Build Variant窗口的列表中,使用鼠标选中 prodDebug或prodRelease ,项目变回会自动进行 Gradle Sync 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
Executing tasks: [:app:generateProdDebugSources]

> Configure project :
...省略
> Task :app:preProdDebugBuild UP-TO-DATE
> Task :app:compileProdDebugAidl NO-SOURCE
> Task :app:compileProdDebugRenderscript UP-TO-DATE
> Task :app:checkProdDebugManifest UP-TO-DATE
> Task :app:generateProdDebugBuildConfig UP-TO-DATE
> Task :app:generateProdDebugSources UP-TO-DATE

BUILD SUCCESSFUL in 0s
6 actionable tasks: 6 up-to-date

执行完成后再来观察项目组织,发现 prod/java 成为了源码集的一部分,mock/java 则变为了普通文件夹
TodoMVP 项目目录
此时若进行编译,便会将 prod/java 下的 Injection 编译到项目中,而 mock 下的文件夹则会被编译器忽略。

小结

Injection 类通过 build vartiant 改变源码集的方式来实现注入,避免了手动修改带来的潜在问题,使得 mock 与 prod 环境间的切换变得尤为方便,不失为一种优雅的方式。

后记

  1. 架构出现的目的是降低项目复杂度
  2. 降低项目复杂度的手段是尽可能的解耦
  3. 解耦的思路是运用设计模式来组织代码
  4. 设计模式遵循的原则是 SOLID

参考链接:
Android官方MVP架构项目解析
SOLID
【第二章】 IoC 之 2.1 IoC基础 ——跟我学Spring3
Leveraging product flavors in Android Studio for hermetic testing