Android官方TODO-MVP项目分析(上)---View 层 Presenter 层以及 Contract 分析
Posted 范二er
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android官方TODO-MVP项目分析(上)---View 层 Presenter 层以及 Contract 分析相关的知识,希望对你有一定的参考价值。
摘要:最近看了一下 google 官方的 sample ,做的是一个 TODO
应用,使用的是 MVP
模式,之前笔者也学习了一段时间的 MVP
,前面写了几篇文章记录学习过程,也有一些思考,最后呈现出来的问题就是 Presenter 层臃肿问题,以及 View
层接口难以管理的问题。比方说 View
层,它是负责 UI
的更新工作,我们希望它里面都是 showXXXZZZ(@Nullable Param p)
这样的更新 UI
状态的方法。在这个 sample
里, google
提供了一种解决接口混乱的方法,用「契约」接口,统一管理 View
层和 Presenter
层的接口,下面就分析下我对这个项目的理解。
原文发表自个人博客:CODE FRAMER BIGZ
项目整体结构分析
因为项目整体使用的是 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 层的接口的,这个类同一个界面对应的View
和Presenter
都要实现, 这样就统一的管理了接口,当我们需要知道 这个View
层,做了哪些操作的时候,只需要看这个Contract
类即可,并且对代码模块的移植也有帮助。整个
data
包下,都是MVP
模式的Model
层,用于从数据源取数据,在这个Sample
里涉及到三种类型的数据,服务器端数据,本地数据库数据和内存缓存中的数据,当然了,这里的服务器端数据时模拟耗时过程的,并没有真正涉及到网络连接的操作。
下面拿 task
包下的类来做说明。(其中 ScrollChildSwipeRefreshLayout
和 TaskFilterType
是业务需求相关的辅助类, 这里暂不做分析。)
tasks 包结构分析
先看看这个包对应的界面长什么样子:
图 2.1 主界面图
左边还有一个 DrawerLayout
:
图 2.2 DrawerLayout
点击 ToolBar
上最右边的 icon
:
图 2.3 Menu 图 1
点击 ToolBar
上次右边的 icon
:
图 2.4 Menu 图 2
当列表中存在任务时:
图 2.5 任务状态为ACTIVE
图 2.6 任务状态为COMPLETED
点击任务,跳转到详情页面,(这个页面不属于这个包下)
图 2.7 任务详情页面
TasksActivity
这是 TasksFragment
的宿主 Activity
,它做的工作就是一些控件的初始化操作,然后实例化 TasksFragment
。
- 初始化
ToolBar
// 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
// 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
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
// Create the presenter 注入到TaskFragment中
mTasksPresenter = new TasksPresenter(
Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
这里同时将 Model
层的对象给注入到了 Presenter
中,这个 TasksRepository
就是属于 Model
层的,后面分析。
- 状态恢复(
onCreate
中,也可以直接在onRestoreInstanceState
方法中操作)
// Load previously saved state, if available.
if (savedInstanceState != null)
TasksFilterType currentFiltering =
(TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
mTasksPresenter.setFiltering(currentFiltering);
- 保存当前显示的
Task
的类别的方法
@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
层取数据的时候,需要展示一个友好交互的页面,提示用户正在加载数据。这里对应接口:
/**
* 展示正在加载中的指示器
*
* @param active true 展示 false 不展示
*/
void setLoadingIndicator(boolean active);
2.当从数据源重拿到数据之后,需要将数据展示到列表上。这里对应接口
/**
* 展示列表中的Task
*
* @param tasks tasks
*/
void showTasks(List<Task> tasks);
3.当从数据源重拿到数据之后产生错误时回调
/**
* 加载错误回调
*/
void showLoadingTasksError();
4.点击右下角的 FloatingActionButton
,会调到创建任务的界面。
/**
* 展示添加任务界面,用于跳转至AddEditTaskActivity
*/
void showAddTask();
5.点击列表中已经存在的任务,会调转至任务详情页面(图 2.7
所示界面),这个操作由点击列表 Item
触发。
/**
* 展示Task的详细信息,跳转至 TaskDetailActivity
* @param taskId taskId
*/
void showTaskDetails`UI`(String taskId);
6.当任务被标记为 COMPLETED
时更新 UI
状态(图2.6所示),这个操作是 checkBox
被点击触发的。
/**
* FilterType 被置为 completed 状态时回调
*/
void showTaskMarkedComplete();
7.当任务被标记为 ACTIVE
时更新 UI
状态 (图2.7所示),这个操作是 checkBox
被点击触发的。
/**
* FilterType 被置为 Active 状态时的回调
*/
void showTaskMarkedActive();
8.当被标记为 COMPLETED
状态的任务被删除时 UI
状态的更新,这个操作是图 2.3
当中所示 Menu
中 Clear Completed
被点击时触发。
/**
* 清除FilterType为Completed状态的Task
*/
void showCompletedTasksCleared();
9.展示所有状态为 ACTIVE
的任务,这个操作是图 2.4
当中所示 Menu
中 Active
被点击时触发
/**
* 展示所有 FilterType 为 Active 的 Task 的回调
*/
void showActiveFilterLabel();
10.没有状态为 ACTIVE
的任务,更新 UI
界面
/**
* 展示没有 FilterType为 Active 时的界面 回调
*/
void showNoActiveTasks();
11.展示所有状态为 COMPLETED
的任务,这个操作是图 2.4
当中所示 Menu
中 COMPLETED
被点击时触发
/**
* 展示所有 FilterType 为 Completed 的Task的回调
*/
void showCompletedFilterLabel();
12.没有状态为 COMPLETED
的任务,更新界面
/**
* 展示没有 FilterType为 Completed 时的界面 回调
*/
void showNoCompletedTasks();
13.展示所有的任务,这个操作是图 2.4
当中所示 Menu
中 All
被点击时触发
/**
* 展示所有 FilterType的回调
*/
void showAllFilterLabel();
14.当前还没有任务展示时,更新 UI
的状态
/**
* 没有Task 时的回调
*/
void showNoTasks();
15.当成功添加了一条任务之后,需要更新 UI
的状态,
/**
* 展示add一条Task成功后的 回调
*/
void showSuccessfullySavedMessage();
16.因为这里的 View
层是用 Fragment
对象实现的,所以这里用于判断当前 Fragment
视图是否还存在
/**
* 当前视图的活跃状态
*
* @return true active<p></p>false destroy
*/
boolean isActive();
17.如图 2.4
所示,这里的显示的效果是使用 PopMenu
做的,所以当我们点击 ToolBar
上次右边的图标时,回调此方法
/**
* 展示 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
层来提供,所以这里需要有一个接口:
/**
* 从 `Model` 层获取数据的回调
*
* @param forceUpdate 是否刷新
*/
void loadTasks(boolean forceUpdate);
2.并且 Presenter
层还需要记录下当前页面的展示哪种类型的数据
/**
* 设置当前列表显示的 Task 的 type
*
* @param requestType @link TasksFilterType
*/
void setFiltering(TasksFilterType requestType);
3.还并且,记下当前页面展示的数据类型,是要在之前 TasksActivity
中的 onSaveInstanceState
方法中获取,然后保存的,所以这里需要提供一个 Getter
方法。
/**
* 拿到当前列表显示的 Task 的 type
*
* @return @link TasksFilterType
*/
TasksFilterType getFiltering();
4.针对 View
层的第 4
条需求,需要点击 FloatingActionButton
跳转至编辑界面,那么针对这个需求,Presenter
层提供一个接口方法给他调用:
/**
* 添加新的 Task 的回调
*/
void addNewTask();
其实这个方法在最终实现的时候,就是调用 View
第 4 个需求的接口:void showAddTask();
然后在这个接口的实现方法就是实例化 Intent
然后 startActivityForResult
。其实完全可以直接在 FloatingActionButton
的 onClick
回调方法里就调用其本身的 showAddTask
方法跳转至编辑页面,但是人家没有这样做,而是调用 presenter
的 addNewTask
方法,通过 Presenter
层的这个方法在去调用 View
层的 showAddTask
方法,为什么做么做?仔细看项目代码可以发现,View
层「不涉及到数据更新和数据获取的改变 UI
状态」类别的接口方法都是被 Presenter
层调用的,而 Presenter
层所有的接口方法都是被 View
层调用的,因为各自的接口方法是需要对方的事件来驱动。 所以为了保证这一特性的统一表现,这里就采取了这样迂回的方式,来跳转至编辑界面。
5.针对 View
层的第 5 条需求,点击列表 Item 的时候,会跳转至详情界面,这个过程和上面点击 FloatingActionButton
一样,不做分析。
/**
* 查看Task详情的回调
*
* @param requestedTask special task
*/
void openTaskDetails(@NonNull Task requestedTask);
6.针对 View
层第 6 条需求,需要将某一条任务标记为 COMPLETED
状态,那么不仅仅是在 UI
上要做改变,还要将数据源中的本条数据给标记为 COMPLETED
状态,所以 Presenter
层要提供这个需求的数据支撑:
/**
* 列表Item的checkBox 从false到true时的回调
*
* @param completedTask special task
*/
void completeTask(@NonNull Task completedTask);
7.针对 View
层的第 7 条需求,和上一条一样,不做分析。
/**
* 列表Item的checkBox 从true到false时的回调
*
* @param activeTask special task
*/
void activateTask(@NonNull Task activeTask);
- 针对
View
层的第 8 条需求,删除标记为COMPLETED
的任务,不仅仅要在UI
上做改变,在数据源中也是需要将它删除的,所以在Presenter
层提供这个需求的数据支撑。
/**
* 清除FilterType为Completed状态的Task
*/
void clearCompletedTasks();
9.针对 View
层的第 15
条需求,showSuccessfullySavedMessage
这个方法是成功添加了一条数据返回此界面之后调用,那么就本应该是在此界面的的 onActivityResult
方法中调用,但是由于和 Presenter 层第 4
个方法一样的原因,这里也是采取了迂回的方式,先通知 Presenter
层,再由 Presenter
层来回调。
/**
* 当一个Task成功添加进来时,返回到TasksFragment时的回调
*
* @param requestCode requestCode
* @param resultCode resultCode
*/
void result(int requestCode, int resultCode);
View 层接口的基类
根据 MVP
模式的原理,View
层是一定持有一个 Presenter
层对象的引用的,所以这里创建一个所有 View
层接口的基类,里面就一个接口方法,用于设置 View
对应的 Presenter
。
public interface Base`View`<T>
/**
* `View`必须要实现的方法,保持对Presenter的引用
* @param presenter
*/
void setPresenter(T presenter);
Presenter 层接口的基类
由于每一次回到 View
层界面的时候,我们都需要展示当前需要被展示的数据(需要被展示的数据是根据当前的 FilterType
来决定的),由于 View
层不涉及数据的缓存,那么我们就需要有一个方法在每一次回到一个 View
层界面的时候都通知 Presenter
层去取数据。
public interface BasePresenter
/**
* Presenter必须实现的方法,用于开始获取数据并且刷新界面,
* 在Fragment的onResume方法中调用
*/
void start();
对于 Fragment
来说,每一次回到一个 Fragment
的时候,onResume
都会调用,就放在这里调用适合。
TasksContract 接口
分析这么多,最终这个接口长这个样子:
public interface TasksContract
interface `View` extends Base`View`<Presenter>
//2.2小结中分析的所有接口
interface Presenter extends BasePresenter
//2.3小结中分析的所有接口
这个接口 View
层和 Presenter
层各自实现其中的子接口。
tasks 包下 View 层和 Presenter 层实现类
接口都定义好了,接下来就是用 TasksFragment
和 TaskPresenter
分别去实现 TasksContract
中的接口了,这部分涉及到具体的业务逻辑,所以不做分析,这里只分析项目结构方面。下面笔者从 google
库中 fork
过来的,添加了部分注释:
小结 View 层和 Presenter 层接口方法
到这里,
View
层和Presenter
层的接口都都分析完了,回过头来再看看,可以发现一个很有意思的地方,在分析完View
层接口之后,笔者将View
层接口归纳为了四类,那么在结合Presenter
层的接口方法看看就会发现,Presenter
层接口方法是针对上面总结的 「涉及到数据更新或者数据获取的改变UI
状态」,「页面跳转」,这两类接口方法的辅助,去除掉「页面跳转」,这个不在MVP
范畴之内,那么剩下的就是,「涉及到数据更新或者数据获取的改变UI
状态」 这个类别下的接口方法了。- 「涉及到数据更新或者数据获取的改变
UI
状态」 这个类别下的接口方法是需要数据作为支撑的,而View
层本身只负责UI
的状态改变,不涉及到数据的获取操作,所以这些数据就需要从Presenter
层中获取。 - 获取到了之后,再到
Presenter
层的接口方法中去回调View
层的 「不涉及到数据更新或者数据获取的改变UI
状态」的接口方法。 - 这么一来,
View
层和Presenter
层通过TasksContract
契约类,完美的契合在一起,这两层的实现类代码中,互相之间都是接口依赖,大大增加了代码的可扩展性。
- 「涉及到数据更新或者数据获取的改变
View
层接口方法的设置完全是从业务逻辑出发的,也就是从需求的角度出发。Presenter
层是服务于Presenter
层,所以它的接口的设置是为了支撑View
层的逻辑。。举个例子:比如说用户点击这个按钮,需要有什么样的一个效果,那么我就针对这个操作,在View
层接口里写一个接口方法;获取数据成功之后,我们需要展示出来,针对这个操作在View
层接口里写一个接口方法;没有获取到任何数据,需要给用户显示一个友好的界面,针对这个操作,又在View
层接口里写一个接口方法。但是这些操作是需要有支撑的,因为View
层本身是不具备它将要更新的UI
所需要的的数据的,所以这时候就是靠Presenter
层来支撑 。这种方式,也让我联想到,如果是团队开发的话,当产品给出原型图了之后,针对每一张原型图当中每一个控件的操作,需要展示的状态,先定好接口,写好
Contract
契约接口,然后团队成员在到各自的分支上并行开发,是否可以大大提高工作效率?这个还有待商榷。
看完这个 Sample 之后的一些感受
如果不看人家
google
工程师的源码,只给我看app
最后的效果,我也能百分百复制出来一个一模一样的,但是我的代码在复用性,鲁棒性,可扩展性方面肯定没有人家的棒,看这个项目的代码真的很舒服,行云流水般的感觉,在编码习惯方面有几点真的十分赞:- 分包很明确,每一包下只有和这个包功能相关的代码,不用到处去找相关类,关看包结构就能得到项目大致结构。
- 包、类、变量、方法、接口等的命名十分规范,命名都是有意义的,更不存在什么
MyXXXX
这种命名方式,观看名字就能知道这个东西是干嘛的。 - 注释十分详细,虽然我在阅读的过程中,添加了中文注释,但是人家本身的英文注释就有很多,每一个文件都有注释用于说明这个文件的用途;用途不是那么显而易见的方法也都有注释,真的是大大减少了我们的阅读难度。这一点很多第三方的框架也做的特别棒,前阵子看
Universal-Image-Loader
的源码,注释也十分详细,并且使用javadoc
。 - 代码在多处做了容错性处理,变量只要在使用的时候,就会去
checkNullOrEmpty
,这个项目里用的是Guava
中的Preconditions
工具类,很方便。
发现自己基础方面不够扎实,整个项目涉及到很多 android 的基础知识,比如说
Activity
和Fragment
的生命周期,重要生命周期方法的作用,调用时机;Activity
和Fragment
之间的通信;关于ToolBar
的使用;关于Menu
菜单的使用;关于android.support.v4.app.NavUtils
这个工具类的使用等等,不一一列举了。总之体现了一个问题,我真的还很菜。contract
接口和Model
层的设计,确实很棒,让传统MVP
模式如虎添翼。希望自己以后再工作当中,从编码习惯方面入手,增强代码的规范性,同时也不能忘了基础的巩固,要学的真的有很多。
- 好像是
Linux
的爸爸说过Read the fuck code !
阅读源码,真的可以学习很多姿势,也能暴露出自己身上存在的很多问题,当然了,前提是这个源码十分优秀,这个是谷歌官方的Sample
库,我感觉维护这个库的人就是官方文档API
示例编写的那一群老哥,因为很多代码的风格和使用的方式,和官方文档上一模一样,比如说在Model
层使用SQLite
的代码,就和官网上的文档一模一样,所以这个源码,必须是很优秀的!
TODO
上面相当于只分析了 View
层和 Presenter
层的结构和实现思路,还有 Model
层没有分析,Model
层是这个 Sample 在传统 MVP
模式当中,除了 Contract
之外,最优雅的设计方式,由于篇幅的原因,Model
层相关的留到下一篇文章分析。
原文发表自个人博客:CODE FRAMER BIGZ
以上是关于Android官方TODO-MVP项目分析(上)---View 层 Presenter 层以及 Contract 分析的主要内容,如果未能解决你的问题,请参考以下文章
Android 官方示例:android-architecture 学习笔记之todo-mvp