Android官方TODO-MVP项目分析(上)---View 层 Presenter 层以及 Contract 分析

摘要:最近看了一下 google 官方的 sample ,做的是一个 TODO 应用,使用的是 MVP 模式,之前笔者也学习了一段时间的 MVP,前面写了几篇文章记录学习过程,也有一些思考,最后呈现出来的问题就是 Presenter 层臃肿问题,以及 View 层接口难以管理的问题。比方说 View 层,它是负责 UI 的更新工作,我们希望它里面都是 showXXXZZZ(@Nullable Param p) 这样的更新 UI 状态的方法。在这个 sample 里, google 提供了一种解决接口混乱的方法,用「契约」接口,统一管理 View 层和 Presenter 层的接口,下面就分析下我对这个项目的理解。

项目整体结构分析

@TODO-MVP项目结构图

因为项目整体使用的是 MVP 模式,所以下面从 MVP 分层的角度来分析;在上面的结构图中,除了 data 包是 Model 层的内容,剩余的四个包里,都是一个包对应一个界面(Activity/Fragment),然后每一个包里有四个类文件,形式分别如下:

  • XxxxActivity:这是 Fragment 的宿主 Acitivity, 同是也是 View 层,但是并没有实现 View 层的接口,主要的 UI 状态更新工作是由 Fragment 来进行的。

  • YyyyFragment:这是 MVP 模式中的 View 层,它实现了 View 层的接口,都是 showXxxYyy() 形式的更新 UI 的回调方法。

  • ZzzzPresenter:这是 MVP 模式中的 Presenter 层,它负责处理 UI 的事件,并且和 Model 层打交道,通过 Model 层拿到数据。

  • PpppContract: 这个类不属于传统 MVP 模式当中的任何一层,它是用于管理 View 层和 Presenter 层的接口的,这个类同一个界面对应的 ViewPresenter 都要实现, 这样就统一的管理了接口,当我们需要知道 这个 View 层,做了哪些操作的时候,只需要看这个 Contract 类即可,并且对代码模块的移植也有帮助。

  • 整个 data 包下,都是 MVP 模式的 Model 层,用于从数据源取数据,在这个 Sample 里涉及到三种类型的数据,服务器端数据,本地数据库数据和内存缓存中的数据,当然了,这里的服务器端数据时模拟耗时过程的,并没有真正涉及到网络连接的操作。

下面拿 task 包下的类来做说明。(其中 ScrollChildSwipeRefreshLayoutTaskFilterType 是业务需求相关的辅助类, 这里暂不做分析。)

tasks 包结构分析

先看看这个包对应的界面长什么样子:

@图 2.1 主界面图|480*800

左边还有一个 DrawerLayout

@图 2.2 DrawerLayout 图|480*800

点击 ToolBar 上最右边的 icon

@图 2.3 Menu 图 1|480*800

点击 ToolBar 上次右边的 icon

@图 2.4 Menu 图 2|480*800

当列表中存在任务时:
@图 2.5 任务状态为ACTIVE |480*800
@图 2.6 任务状态为COMPLETED|480*800

点击任务,跳转到详情页面,(这个页面不属于这个包下)
@图 2.7 任务详情页面 |480*800

TasksActivity

这是 TasksFragment 的宿主 Activity,它做的工作就是一些控件的初始化操作,然后实例化 TasksFragment

  • 初始化 ToolBar
1
2
3
4
5
6
7
// Set up the toolbar.
Toolbar toolbar = (Toolbar) find`View`ById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
ab.setHomeAsUpIndicator(R.drawable.ic_menu);
ab.setDisplayHomeAsUpEnabled(true);
  • 初始化 Navigation Drawer
1
2
3
4
5
6
7
8
// Set up the navigation drawer.
mDrawerLayout = (DrawerLayout) find`View`ById(R.id.drawer_layout);
mDrawerLayout.setStatusBarBackground(R.color.colorPrimaryDark);
Navigation`View` navigation`View` = (Navigation`View`) find`View`ById(R.id.nav_`View`);
if (navigation`View` != null) {
setupDrawerContent(navigation`View`);
}
  • 初始化对应的 Fragment
1
2
3
4
5
6
7
8
9
TasksFragment tasksFragment =
(TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (tasksFragment == null) {
// Create the fragment
tasksFragment = TasksFragment.newInstance();
ActivityUtils.addFragmentToActivity(
getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
}
  • Presenter 注入 View
1
2
3
4
// Create the presenter 注入到TaskFragment中
mTasksPresenter = new TasksPresenter(
Injection.provideTasksRepository(getApplicationContext()), tasksFragment);

这里同时将 Model 层的对象给注入到了 Presenter 中,这个 TasksRepository 就是属于 Model 层的,后面分析。

  • 状态恢复(onCreate 中,也可以直接在 onRestoreInstanceState 方法中操作)
1
2
3
4
5
6
7
// Load previously saved state, if available.
if (savedInstanceState != null) {
TasksFilterType currentFiltering =
(TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
mTasksPresenter.setFiltering(currentFiltering);
}
  • 保存当前显示的 Task 的类别的方法
1
2
3
4
5
6
7
8
9
@Override
public void onSaveInstanceState(Bundle outState) {
//此处需要保存的信息是当前 Task 列表展示界面展示的 Filter Type 信息,
// 目的是为了下一次重其他的页面跳回到此页面时,能够正确的显示 对应 Filter Type 的 Task
outState.putSerializable(CURRENT_FILTERING_KEY, mTasksPresenter.getFiltering());
super.onSaveInstanceState(outState);
}

从一个 Activity 跳到另外一个 Activity 的时候会调用,用于存储当前 Activity 正在显示的 Task 的类别,类别有三种,分别是 COMPLETED_TASK , ACTIVT_TASK , ALL_TASK; 很好理解,就是用于辨别当前界面是显示已经完成的 Task 还是显示暂未完成的,还是都显示,用于下一次从另外页面回到当前页面的时候,显示的是用户上一次的操作。

  • 剩下的就是 Mene 的初始化 和点击时间的处理了。这里就不贴出代码了。

TasksContract 中 View 层接口分析

之前说过, TasksContract 适用于管理 View 层和 Presenter 层的接口的契约接口,我们希望 View 层的方法都是类似于 showXxxZzz() 形式的方法,用于改变 UI 的状态,那么根据上面的截面图,我们分析一下这里的 View 层需要改变哪些状态。

此处涉及到具体的业务逻辑,项目需求,包括每一个控件的点击事件,每一种状态的显示页面 。具体的思路就是,将每一个改变 UI 状态的操作都抽象成接口方法。

  1. 当我们从 Model 层取数据的时候,需要展示一个友好交互的页面,提示用户正在加载数据。这里对应接口:
1
2
3
4
5
6
7
/**
* 展示正在加载中的指示器
*
* @param active true 展示 false 不展示
*/
void setLoadingIndicator(boolean active);
  1. 当从数据源重拿到数据之后,需要将数据展示到列表上。这里对应接口
1
2
3
4
5
6
7
/**
* 展示列表中的Task
*
* @param tasks tasks
*/
void showTasks(List<Task> tasks);
  1. 当从数据源重拿到数据之后产生错误时回调
1
2
3
4
5
/**
* 加载错误回调
*/
void showLoadingTasksError();
  1. 点击右下角的 FloatingActionButton,会调到创建任务的界面。
1
2
3
4
5
/**
* 展示添加任务界面,用于跳转至AddEditTaskActivity
*/
void showAddTask();
  1. 点击列表中已经存在的任务,会调转至任务详情页面(图 2.7所示界面),这个操作由点击列表 Item 触发。
1
2
3
4
5
6
/**
* 展示Task的详细信息,跳转至 TaskDetailActivity
* @param taskId taskId
*/
void showTaskDetails`UI`(String taskId);
  1. 当任务被标记为 COMPLETED 时更新 UI 状态(图2.6所示),这个操作是 checkBox 被点击触发的。
1
2
3
4
5
/**
* FilterType 被置为 completed 状态时回调
*/
void showTaskMarkedComplete();
  1. 当任务被标记为 ACTIVE 时更新 UI 状态 (图2.7所示),这个操作是 checkBox 被点击触发的。
1
2
3
4
5
/**
* FilterType 被置为 Active 状态时的回调
*/
void showTaskMarkedActive();
  1. 当被标记为 COMPLETED 状态的任务被删除时 UI 状态的更新,这个操作是图 2.3 当中所示 MenuClear Completed 被点击时触发。
1
2
3
4
5
/**
* 清除FilterType为Completed状态的Task
*/
void showCompletedTasksCleared();
  1. 展示所有状态为 ACTIVE 的任务,这个操作是图 2.4 当中所示 MenuActive 被点击时触发
1
2
3
4
5
/**
* 展示所有 FilterType 为 Active 的 Task 的回调
*/
void showActiveFilterLabel();
  1. 没有状态为 ACTIVE 的任务,更新 UI 界面
1
2
3
4
5
/**
* 展示没有 FilterType为 Active 时的界面 回调
*/
void showNoActiveTasks();
  1. 展示所有状态为 COMPLETED 的任务,这个操作是图 2.4 当中所示 MenuCOMPLETED 被点击时触发
1
2
3
4
5
/**
* 展示所有 FilterType 为 Completed 的Task的回调
*/
void showCompletedFilterLabel();
  1. 没有状态为 COMPLETED 的任务,更新界面
1
2
3
4
5
/**
* 展示没有 FilterType为 Completed 时的界面 回调
*/
void showNoCompletedTasks();
  1. 展示所有的任务,这个操作是图 2.4 当中所示 MenuAll 被点击时触发
1
2
3
4
5
/**
* 展示所有 FilterType的回调
*/
void showAllFilterLabel();
  1. 当前还没有任务展示时,更新 UI 的状态
1
2
3
4
5
/**
* 没有Task 时的回调
*/
void showNoTasks();
  1. 当成功添加了一条任务之后,需要更新 UI 的状态,
1
2
3
4
5
/**
* 展示add一条Task成功后的 回调
*/
void showSuccessfullySavedMessage();
  1. 因为这里的 View 层是用 Fragment 对象实现的,所以这里用于判断当前 Fragment 视图是否还存在
1
2
3
4
5
6
7
/**
* 当前视图的活跃状态
*
* @return true active<p></p>false destroy
*/
boolean isActive();
  1. 如图 2.4 所示,这里的显示的效果是使用 PopMenu 做的,所以当我们点击 ToolBar 上次右边的图标时,回调此方法
1
2
3
4
5
/**
* 展示 Toolbar上面的Menu的 选择 展示 FilterType 的popmenu
*/
void showFilteringPopUpMenu();

可以发现 View 层接口大体分为四类:

  • 涉及到数据更新或者数据获取的改变 UI 状态,第 6,7,8,9 ,11 ,13,15。
  • 页面跳转,第 4 ,5 两个用于启动其他 Activity 的。
  • 不涉及到数据更新和数据获取的改变 UI 状态,1,2,3,10,12 ,14。其中第 2 条只是展示已经获取到的数据,没有涉及到数据的获取和改变。
  • 辅助方法 16,17

Contract 中 Presenter 层接口分析

TasksContract 当中,不仅仅定义了 View 层的接口,并且还定义了 Presenter 层的接口。这一层的接口肯定是服务于 View 层的,应为 Presenter 层需要响应 View 层的事件,然后和 Model 层交互,然后再根据和 Model 层交互的接口,通知 View 层更新对应的 UI 状态。所以 Presenter 层接口的设置肯定与上面 View 层的 UI 状态改变接口有关,下面来分析一下:

  1. 针对 View 层的第 9, 11, 13 ,条需求,分别需要展示 ACTIVE COMPLETED 和所有状态的数据, 那么这个数据从哪儿来呢?就需要 Presenter 层来提供,所以这里需要有一个接口:
1
2
3
4
5
6
7
/**
* 从 `Model` 层获取数据的回调
*
* @param forceUpdate 是否刷新
*/
void loadTasks(boolean forceUpdate);
  1. 并且 Presenter 层还需要记录下当前页面的展示哪种类型的数据
1
2
3
4
5
6
7
/**
* 设置当前列表显示的 Task 的 type
*
* @param requestType {@link TasksFilterType}
*/
void setFiltering(TasksFilterType requestType);
  1. 还并且,记下当前页面展示的数据类型,是要在之前 TasksActivity 中的 onSaveInstanceState 方法中获取,然后保存的,所以这里需要提供一个 Getter 方法。
1
2
3
4
5
6
7
/**
* 拿到当前列表显示的 Task 的 type
*
* @return {@link TasksFilterType}
*/
TasksFilterType getFiltering();
  1. 针对 View 层的第 4 条需求,需要点击 FloatingActionButton 跳转至编辑界面,那么针对这个需求,Presenter 层提供一个接口方法给他调用:
1
2
3
4
5
/**
* 添加新的 Task 的回调
*/
void addNewTask();

其实这个方法在最终实现的时候,就是调用 View 第 4 个需求的接口:void showAddTask(); 然后在这个接口的实现方法就是实例化 Intent 然后 startActivityForResult。其实完全可以直接在 FloatingActionButtononClick 回调方法里就调用其本身的 showAddTask 方法跳转至编辑页面,但是人家没有这样做,而是调用 presenteraddNewTask 方法,通过 Presenter 层的这个方法在去调用 View 层的 showAddTask 方法,为什么做么做?仔细看项目代码可以发现,View 层「不涉及到数据更新和数据获取的改变 UI 状态」类别的接口方法都是被 Presenter 层调用的,而 Presenter 层所有的接口方法都是被 View 层调用的,因为各自的接口方法是需要对方的事件来驱动。 所以为了保证这一特性的统一表现,这里就采取了这样迂回的方式,来跳转至编辑界面。

  1. 针对 View 层的第 5 条需求,点击列表 Item 的时候,会跳转至详情界面,这个过程和上面点击 FloatingActionButton 一样,不做分析。
1
2
3
4
5
6
7
/**
* 查看Task详情的回调
*
* @param requestedTask special task
*/
void openTaskDetails(@NonNull Task requestedTask);
  1. 针对 View 层第 6 条需求,需要将某一条任务标记为 COMPLETED 状态,那么不仅仅是在 UI 上要做改变,还要将数据源中的本条数据给标记为 COMPLETED 状态,所以 Presenter 层要提供这个需求的数据支撑:
1
2
3
4
5
6
7
/**
* 列表Item的checkBox 从false到true时的回调
*
* @param completedTask special task
*/
void completeTask(@NonNull Task completedTask);
  1. 针对 View 层的第 7 条需求,和上一条一样,不做分析。
1
2
3
4
5
6
7
/**
* 列表Item的checkBox 从true到false时的回调
*
* @param activeTask special task
*/
void activateTask(@NonNull Task activeTask);
  1. 针对 View 层的第 8 条需求,删除标记为 COMPLETED 的任务,不仅仅要在 UI 上做改变,在数据源中也是需要将它删除的,所以在 Presenter 层提供这个需求的数据支撑。
1
2
3
4
5
/**
* 清除FilterType为Completed状态的Task
*/
void clearCompletedTasks();
  1. 针对 View 层的第 15 条需求,showSuccessfullySavedMessage 这个方法是成功添加了一条数据返回此界面之后调用,那么就本应该是在此界面的的 onActivityResult 方法中调用,但是由于和 Presenter 层第 4 个方法一样的原因,这里也是采取了迂回的方式,先通知 Presenter 层,再由 Presenter 层来回调。
1
2
3
4
5
6
7
8
/**
* 当一个Task成功添加进来时,返回到TasksFragment时的回调
*
* @param requestCode requestCode
* @param resultCode resultCode
*/
void result(int requestCode, int resultCode);

View 层接口的基类

根据 MVP 模式的原理,View 层是一定持有一个 Presenter 层对象的引用的,所以这里创建一个所有 View 层接口的基类,里面就一个接口方法,用于设置 View 对应的 Presenter

1
2
3
4
5
6
7
8
9
public interface Base`View`<T> {
/**
* `View`必须要实现的方法,保持对Presenter的引用
* @param presenter
*/
void setPresenter(T presenter);
}

Presenter 层接口的基类

由于每一次回到 View 层界面的时候,我们都需要展示当前需要被展示的数据(需要被展示的数据是根据当前的 FilterType 来决定的),由于 View 层不涉及数据的缓存,那么我们就需要有一个方法在每一次回到一个 View 层界面的时候都通知 Presenter 层去取数据。

1
2
3
4
5
6
7
8
9
public interface BasePresenter {
/**
* Presenter必须实现的方法,用于开始获取数据并且刷新界面,
* 在Fragment的onResume方法中调用
*/
void start();
}

对于 Fragment 来说,每一次回到一个 Fragment 的时候,onResume 都会调用,就放在这里调用适合。

TasksContract 接口

分析这么多,最终这个接口长这个样子:

1
2
3
4
5
6
7
8
9
10
11
public interface TasksContract {
interface `View` extends Base`View`<Presenter> {
//2.2小结中分析的所有接口
}
interface Presenter extends BasePresenter {
//2.3小结中分析的所有接口
}
}

这个接口 View 层和 Presenter 层各自实现其中的子接口。

tasks 包下 View 层和 Presenter 层实现类

接口都定义好了,接下来就是用 TasksFragmentTaskPresenter 分别去实现 TasksContract 中的接口了,这部分涉及到具体的业务逻辑,所以不做分析,这里只分析项目结构方面。下面笔者从 google 库中 fork 过来的,添加了部分注释:

小结 View 层和 Presenter 层接口方法

  1. 到这里,View 层和 Presenter 层的接口都都分析完了,回过头来再看看,可以发现一个很有意思的地方,在分析完 View 层接口之后,笔者将 View 层接口归纳为了四类,那么在结合 Presenter 层的接口方法看看就会发现,Presenter 层接口方法是针对上面总结的 「涉及到数据更新或者数据获取的改变 UI 状态」,「页面跳转」,这两类接口方法的辅助,去除掉「页面跳转」,这个不在 MVP 范畴之内,那么剩下的就是,「涉及到数据更新或者数据获取的改变 UI 状态」 这个类别下的接口方法了。
  • 「涉及到数据更新或者数据获取的改变 UI 状态」 这个类别下的接口方法是需要数据作为支撑的,而 View 层本身只负责 UI 的状态改变,不涉及到数据的获取操作,所以这些数据就需要从 Presenter 层中获取。
  • 获取到了之后,再到 Presenter 层的接口方法中去回调 View 层的 「不涉及到数据更新或者数据获取的改变 UI 状态」的接口方法。
  • 这么一来,View 层和 Presenter 层通过 TasksContract 契约类,完美的契合在一起,这两层的实现类代码中,互相之间都是接口依赖,大大增加了代码的可扩展性。
  1. View 层接口方法的设置完全是从业务逻辑出发的,也就是从需求的角度出发。 Presenter 层是服务于 Presenter 层,所以它的接口的设置是为了支撑 View 层的逻辑。。举个例子:比如说用户点击这个按钮,需要有什么样的一个效果,那么我就针对这个操作,在 View 层接口里写一个接口方法;获取数据成功之后,我们需要展示出来,针对这个操作在 View 层接口里写一个接口方法;没有获取到任何数据,需要给用户显示一个友好的界面,针对这个操作,又在 View 层接口里写一个接口方法。但是这些操作是需要有支撑的,因为 View 层本身是不具备它将要更新的 UI 所需要的的数据的,所以这时候就是靠 Presenter 层来支撑 。

  2. 这种方式,也让我联想到,如果是团队开发的话,当产品给出原型图了之后,针对每一张原型图当中每一个控件的操作,需要展示的状态,先定好接口,写好 Contract 契约接口,然后团队成员在到各自的分支上并行开发,是否可以大大提高工作效率?这个还有待商榷。

看完这个 Sample 之后的一些感受

  1. 如果不看人家 google 工程师的源码,只给我看 app 最后的效果,我也能百分百复制出来一个一模一样的,但是我的代码在复用性,鲁棒性,可扩展性方面肯定没有人家的棒,看这个项目的代码真的很舒服,行云流水般的感觉,在编码习惯方面有几点真的十分赞:
  • 分包很明确,每一包下只有和这个包功能相关的代码,不用到处去找相关类,关看包结构就能得到项目大致结构。
  • 包、类、变量、方法、接口等的命名十分规范,命名都是有意义的,更不存在什么 MyXXXX 这种命名方式,观看名字就能知道这个东西是干嘛的。
  • 注释十分详细,虽然我在阅读的过程中,添加了中文注释,但是人家本身的英文注释就有很多,每一个文件都有注释用于说明这个文件的用途;用途不是那么显而易见的方法也都有注释,真的是大大减少了我们的阅读难度。这一点很多第三方的框架也做的特别棒,前阵子看 Universal-Image-Loader 的源码,注释也十分详细,并且使用 javadoc
  • 代码在多处做了容错性处理,变量只要在使用的时候,就会去 checkNullOrEmpty,这个项目里用的是 Guava 中的 Preconditions 工具类,很方便。
  1. 发现自己基础方面不够扎实,整个项目涉及到很多 Android 的基础知识,比如说 ActivityFragment 的生命周期,重要生命周期方法的作用,调用时机;ActivityFragment 之间的通信;关于 ToolBar 的使用;关于 Menu 菜单的使用;关于 android.support.v4.app.NavUtils 这个工具类的使用等等,不一一列举了。总之体现了一个问题,我真的还很菜。

  2. contract 接口和 Model 层的设计,确实很棒,让传统 MVP 模式如虎添翼。

  3. 希望自己以后再工作当中,从编码习惯方面入手,增强代码的规范性,同时也不能忘了基础的巩固,要学的真的有很多。

  4. 好像是 Linux 的爸爸说过 Read the fuck code !阅读源码,真的可以学习很多姿势,也能暴露出自己身上存在的很多问题,当然了,前提是这个源码十分优秀,这个是谷歌官方的 Sample 库,我感觉维护这个库的人就是官方文档 API 示例编写的那一群老哥,因为很多代码的风格和使用的方式,和官方文档上一模一样,比如说在 Model 层使用 SQLite 的代码,就和官网上的文档一模一样,所以这个源码,必须是很优秀的!

TODO

上面相当于只分析了 View 层和 Presenter 层的结构和实现思路,还有 Model 层没有分析,Model 层是这个 Sample 在传统 MVP 模式当中,除了 Contract 之外,最优雅的设计方式,由于篇幅的原因,Model 层相关的留到下一篇文章分析。

共82.3k字
0%
.gt-container a{border-bottom: none;}