Android 官方架构示例android-architecture之todo-mvp深入解析

google在GitHub上开源了android-architecture项目,包含了MVP、MVVM等架构的示例项目,今天我们从todo‑mvp开始入手,研究里面代码的具体实现

项目地址

todo-mvp项目地址

应用功能介绍

了解一个项目的主要功能最快的方法就是直接安装,然后运行,就可以知道主要有哪些页面,有哪些功能

todo示例项目是一个代办事项的简单App,有四个页面

  • 待办列表页
  • 待办详情页
  • 待办编辑页
  • 待办统计页

imgClick and drag to move imgClick and drag to move

imgClick and drag to move imgClick and drag to move

项目结构

我们主要研究MVP代码的实现,androidTest、androidTestMock、mock、test下是测试的代码,不在我们本次的讨论范围内,下次会另开一篇文章讨论。

项目分包主要以业务模块来划分

  • tasks包:待办列表模块
  • taskdetail包:待办详情模块
  • addedittask包:新建/编辑待办模块
  • statistics包:待办统计模块
  • data包:跟MVP中V有关的模块
  • util包:工具模块
  • BaseView接口:MVP中V的基础接口
  • BasePresenter包接口:MVP中P的基础接口

我们先看下BaseView的定义,每一个具体的业务模块的View都要实现该接口,通过setPresenter方法持有Presenter的对象引用,然后通过Presenter对象调用具体的业务逻辑

1
2
3
public interface BaseView<T> {
void setPresenter(T presenter);
}

Click and drag to move

我们再看下BasePresenter的定义,每一个具体业务模块的Presenter都需要实现该接口,需实现start()方法做一些初始化的业务,而该方法一般是在Fragment的onResume中调用(单不是绝对的,视具体业务而定)

1
2
3
public interface BasePresenter {
void start();
}

Click and drag to move

imgClick and drag to move

MVP架构概览

如下图所示,我们具体展开每一个业务模块,可以发现每个模块的设计都是一样的,主要有四个类型的类

  • Contract:合同类,这是一个接口,里面又定义了两个子接口View和Presenter
  • Activity:只是一个Fragment的容器,具体的View实现交个Fragment
  • Fragment:实现Contract中定义的View接口,承担View的角色,负责页面的显示刷新交互,持有Presenter的引用,调用Presenter的相关业务实现
  • Presenter:实现Contract中定义的Presenter接口,承担Presenter角色,负责具体的业务逻辑,在Presenter中会可能会调用Model对数据进行操作,然后通过持有的View对象的引用回调View,操作更新页面

imgClick and drag to move

V和P具体代码实现

由于每一个具体业务模块的实现代码都大同小异,我们选取待办编辑这一业务模块来看看具体的实现代码

待办列表模块的业务功能比较多,代码量也最多,为了快速入门,我们选择待办编辑代码量比较适中,也不会像待办统计也代码量太少。

待办编辑模块主要有两个功能,一个是新建待办然后编辑保存,一个是编辑已有的待办然后保存

待办编辑模块主要有如下四个类,我们按顺序讲解。

  • AddEditTaskContract
  • AddEditTaskActivity
  • AddEditTaskFragment
  • AddEditTaskPresenter

AddEditTaskContract

Contract定义了View和Presenter之间的一组协议

View继承BaseView,负责UI的操作更新

Presenter继承BasePresenter,负责具体的业务逻辑,有可能会调用Model的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface AddEditTaskContract {
interface View extends BaseView<Presenter> {
// 保存待办时如果是空任务的情况的显示提醒
void showEmptyTaskError();
// 保存成功后返回待办列表页面
void showTasksList();
// 设置待办标题
void setTitle(String title);
// 设置待办说明
void setDescription(String description);
// 当前View是否已销毁,一般在Presenter中更新UI前都要调用该方法判断
boolean isActive();
}

interface Presenter extends BasePresenter {
// 保存待办
void saveTask(String title, String description);
// 查询已有的待办事项,在初始进入的时候且是已有待办的情况下调用
void populateTask();
// 返回一个标志字段判断是否需要重新加载数据
boolean isDataMissing();
}
}

Click and drag to move

AddEditTaskActivity

Activity的功能比较简单,主要是负责加载Fragment和创建Presenter对象

不承担View和Presenter的功能

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
public class AddEditTaskActivity extends AppCompatActivity {

public static final int REQUEST_ADD_TASK = 1;
public static final String SHOULD_LOAD_DATA_FROM_REPO_KEY = "SHOULD_LOAD_DATA_FROM_REPO_KEY";
private AddEditTaskPresenter mAddEditTaskPresenter;
private ActionBar mActionBar;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.addtask_act);

// 一些ToolBar初始化设置
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mActionBar = getSupportActionBar();
mActionBar.setDisplayHomeAsUpEnabled(true);
mActionBar.setDisplayShowHomeEnabled(true);

// 如果是从待办列表页面点击某个具体待办跳转过来,会传过来待办的id
String taskId = getIntent().getStringExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID);
// 设置标题,如果是新建待办显示“New TO-DO”,如果是编辑已有待办,显示“Edit TO-DO”
setToolbarTitle(taskId);


AddEditTaskFragment addEditTaskFragment = (AddEditTaskFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (addEditTaskFragment == null) {
addEditTaskFragment = AddEditTaskFragment.newInstance();
// 编辑已有任务需将待办id传递给Fragment
if (getIntent().hasExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID)) {
Bundle bundle = new Bundle();
bundle.putString(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId);
addEditTaskFragment.setArguments(bundle);
}
// 将Fragment添加到Activity中
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), addEditTaskFragment, R.id.contentFrame);
}

boolean shouldLoadDataFromRepo = true;
// 防止设置更改(如横竖屏切换)情况下又重复请求一次数据
if (savedInstanceState != null) {
// Data might not have loaded when the config change happen, so we saved the state.
shouldLoadDataFromRepo = savedInstanceState.getBoolean(SHOULD_LOAD_DATA_FROM_REPO_KEY);
}

// 实例化Presenter,传入addEditTaskFragment持有View对象
mAddEditTaskPresenter = new AddEditTaskPresenter(
taskId,
Injection.provideTasksRepository(getApplicationContext()),
addEditTaskFragment,
shouldLoadDataFromRepo);
}

private void setToolbarTitle(@Nullable String taskId) {
if(taskId == null) {
mActionBar.setTitle(R.string.add_task);
} else {
mActionBar.setTitle(R.string.edit_task);
}
}

@Override
protected void onSaveInstanceState(Bundle outState) {
// Save the state so that next time we know if we need to refresh data.
outState.putBoolean(SHOULD_LOAD_DATA_FROM_REPO_KEY, mAddEditTaskPresenter.isDataMissing());
super.onSaveInstanceState(outState);
}

@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}

@VisibleForTesting
public IdlingResource getCountingIdlingResource() {
return EspressoIdlingResource.getIdlingResource();
}
}

Click and drag to move

AddEditTaskFragment

AddEditTaskFragment功能很简单,就是编辑待办事项,可以是编辑新建的待办,也可以是编辑已有的待办,然后最后保存返回待办列表页面。

AddEditTaskFragment通过setPresenter持有AddEditTaskPresenter对象示例

在onResume的地方调用Presenter请求已有的任务(如果是已有任务的情况下)

在右下角完成按钮点击的时候调用Presenter的saveTask()方法保存任务

View的主要功能就是:

  1. 事件触发后调用Presenter处理具体的业务
  2. 实现Contract中定义的方法操作显示UI(等待Presenter业务处理完的回调)
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
82
83
84
85
public class AddEditTaskFragment extends Fragment implements AddEditTaskContract.View {

public static final String ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID";
private AddEditTaskContract.Presenter mPresenter;
private TextView mTitle;
private TextView mDescription;

public static AddEditTaskFragment newInstance() {
return new AddEditTaskFragment();
}

public AddEditTaskFragment() {
// Required empty public constructor
}

@Override
public void onResume() {
super.onResume();
// Presenter.start的具体实现中,如果是已有的待办事项,会先查询
mPresenter.start();
}

// 设置Presenter,持有该对象的引用
@Override
public void setPresenter(@NonNull AddEditTaskContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);

// 右下角的完成编辑保存按钮
FloatingActionButton fab = getActivity().findViewById(R.id.fab_edit_task_done);
fab.setImageResource(R.drawable.ic_done);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 调用Presenter实现具体保存业务逻辑
mPresenter.saveTask(mTitle.getText().toString(), mDescription.getText().toString());
}
});
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.addtask_frag, container, false);
mTitle = root.findViewById(R.id.add_task_title);
mDescription = root.findViewById(R.id.add_task_description);
setHasOptionsMenu(true);
return root;
}

// 保存待办时如果是空任务的情况的显示提醒
@Override
public void showEmptyTaskError() {
Snackbar.make(mTitle, getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show();
}

// 保存成功后返回待办列表页面
@Override
public void showTasksList() {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}

// 设置待办标题
@Override
public void setTitle(String title) {
mTitle.setText(title);
}

// 设置待办说明
@Override
public void setDescription(String description) {
mDescription.setText(description);
}

// 当前View是否已销毁,一般在Presenter中更新UI前都要调用该方法判断
@Override
public boolean isActive() {
return isAdded();
}
}

Click and drag to move

AddEditTaskPresenter

AddEditTaskPresenter实现了AddEditTaskContract.Presenter中定义的接口

在View中触发事件,然后为了View跟Model层的解耦,甩锅给Presenter让Presenter调用Model执行一些增删改查等操作(本地数据库操作或者网络请求操作),最后再通过Presenter回调View层处理更新UI。整个过程View跟Model是解耦的。

所以Presenter就是View和Model之间的桥梁,是个跑腿的,举个栗子:

View说:喂,Presenter,去Model那边帮我买一瓶酱油回来

Presenter:屁颠屁颠的跑去Model那边买了瓶酱油,然后再跑回去拿给View

View:拿到酱油后炒了一盘炒饭发到朋友圈显示出来

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* Listens to user actions from the UI ({@link AddEditTaskFragment}), retrieves the data and updates
* the UI as required.
*/
public class AddEditTaskPresenter implements AddEditTaskContract.Presenter, TasksDataSource.GetTaskCallback {

@NonNull
private final TasksDataSource mTasksRepository;

@NonNull
private final AddEditTaskContract.View mAddTaskView;

@Nullable
private String mTaskId;

private boolean mIsDataMissing;

/**
* Creates a presenter for the add/edit view.
*
* @param taskId ID of the task to edit or null for a new task
* @param tasksRepository a repository of data for tasks
* @param addTaskView the add/edit view
* @param shouldLoadDataFromRepo whether data needs to be loaded or not (for config changes)
*/
public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
@NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) {
mTaskId = taskId;
mTasksRepository = checkNotNull(tasksRepository);
mAddTaskView = checkNotNull(addTaskView);
mIsDataMissing = shouldLoadDataFromRepo;

mAddTaskView.setPresenter(this);
}

// 如果是已有的待办,查询该待办的信息
@Override
public void start() {
if (!isNewTask() && mIsDataMissing) {
populateTask();
}
}

// 完成编辑
@Override
public void saveTask(String title, String description) {
if (isNewTask()) {
createTask(title, description);
} else {
updateTask(title, description);
}
}

@Override
public void populateTask() {
if (isNewTask()) {
throw new RuntimeException("populateTask() was called but task is new.");
}
// 通过Repository数据仓库根据taskId查询该待办信息,然后在callback中回调UI显示待办信息
mTasksRepository.getTask(mTaskId, this);
}

@Override
public void onTaskLoaded(Task task) {
// 查询成功后回调UI显示,显示前先判断View是否已销毁
// The view may not be able to handle UI updates anymore
if (mAddTaskView.isActive()) {
mAddTaskView.setTitle(task.getTitle());
mAddTaskView.setDescription(task.getDescription());
}
mIsDataMissing = false;
}

@Override
public void onDataNotAvailable() {
// 查询失败后回调UI显示,显示前先判断View是否已销毁
// The view may not be able to handle UI updates anymore
if (mAddTaskView.isActive()) {
mAddTaskView.showEmptyTaskError();
}
}

@Override
public boolean isDataMissing() {
return mIsDataMissing;
}

// 判断是已有待办或者是新建待办
private boolean isNewTask() {
return mTaskId == null;
}

// 通过Model保存新创建的待办,然后返回待办列表页面
private void createTask(String title, String description) {
Task newTask = new Task(title, description);
if (newTask.isEmpty()) {
mAddTaskView.showEmptyTaskError();
} else {
mTasksRepository.saveTask(newTask);
mAddTaskView.showTasksList();
}
}

// 通过Model保存已有待办的更新,然后返回待办列表页面
private void updateTask(String title, String description) {
if (isNewTask()) {
throw new RuntimeException("updateTask() was called but task is new.");
}
mTasksRepository.saveTask(new Task(title, description, mTaskId));
mAddTaskView.showTasksList(); // After an edit, go back to the list.
}
}

Click and drag to move

Model设计

本示例中Mode层在data包下,又分为local和remote两个包。

local表示本地数据库数据

remote表示远程服务端数据,但是本示例并未真正实现服务端接口的请求,只是用一个LinkedHashMap增删改查待办任务,然后用Handler.postDelay来模拟异步请求

TaskDatasource定义了数据源的接口,包括获取所有待办事项、获取单个待办事项、创建待办事项、完成待办事项等

TaskLocalDataSource实现了TaskDatasource,实现了本地的数据操作

TaskRemoteDataSource实现了TaskDataSource,模拟了网络异步的数据操作

TaskRepository代表的就是View层,Presenter中持有的View对象就是TaskRepository,TaskRepository负责提供数据和对数据进行操作,然后把结果返回给Presenter,Presenter再回调View更新视图。

TaskRepository也实现了TaskDatasource接口,内部同时持有TaskLocalDataSource和TaskRemoteDataSource两种数据源,同时还有一个Map内存缓存,对待办数据进行增加、删除、修改等操作是会同时对内存缓存、TaskLocalDataSource、TaskRemoteDataSource三种数据源进行操作。

imgClick and drag to move

总结

盗用一张百科的图片,如有侵权请联系删除

imgClick and drag to move

  • View和Model彻底解耦
  • Presenter是View和Model之间的桥梁

一个完整流程包含以下四个步骤:

  1. View中事件触发
  2. Presenter调用Model请求处理数据
  3. Model处理完数据返回给Presenter
  4. Presenter回调View更新UI