Google Samples 学习之 todo-mvp(五)
本文着重对 tasks 目录下的代码进行分析,这一部分是 app 中最重要也最复杂的代码。
前置文章: Google Samples 学习之 todo-mvp(一) Google Samples 学习之 todo-mvp(二) Google Samples 学习之 todo-mvp(三) Google Samples 学习之 todo-mvp(四)
结构分析
tasks 目录的结构如下图所示:
各个文件的职责和功能为:
- ScrollChildSwipeRefreshLayout:自定义下拉刷新布局
- TasksActivity 和 TasksFragment:组成本功能模块的 View 层代码
- TasksContract:定义 Preseter 和 View 的功能与访问方法
- TasksFilterType:定义菜单栏筛选操作所需的枚举类型
- TasksPresenter:本模块的 Presenter 层代码
代码分析
TasksContract
View 层接口方法:
- setLoadingIndicator(boolean active):显示 loading 提示符,提示用户等待
- showTasks(List
tasks):显示任务列表 - showAddTask():进入添加任务界面
- showTaskDetailsUi(String taskId):进入任务内容浏览界面
- showTaskMarkedComplete():显示已完成任务列表
- showTaskMarkedActive():显示未完成任务列表
- showCompletedTasksCleared():显示清空已完成任务后的反馈结果
- showLoadingTasksError():显示载入错误
- showNoTasks():任务列表为空的界面
- showActiveFilterLabel():显示未完成任务筛选功能
- showCompletedFilterLabel():显示已完成任务筛选功能
- showAllFilterLabel():显示无条件筛选功能
- showNoActiveTasks():显示没有未完成任务
- showNoCompletedTasks():显示没有已完成任务
- showSuccessfullySavedMessage():显示储存成功信息
- boolean isActive():是否是未完成任务
- showFilteringPopUpMenu():显示任务筛选功能菜单
Presenter 层接口方法:
- result(int requestCode, int resultCode):显示添加新任务的结果
- loadTasks(boolean forceUpdate):载入所有任务
- addNewTask():添加新任务
- openTaskDetails(@NonNull Task requestedTask):获取单个任务的信息
- completeTask(@NonNull Task completedTask):标记任务为已完成
- activateTask(@NonNull Task activeTask):标记任务为未完成
- clearCompletedTasks():清空已完成任务
- setFiltering(TasksFilterType requestType):筛选任务
- getFiltering():获取筛选的条件类型
TasksPresenter
result
因为添加新任务的页面在添加任务成功后会自动关闭,然后跳转回任务列表页面,所有添加结果的反馈要在任务列表页面完成。 调用 View 层的 showSuccessfullySavedMessage 方法显示结果。
loadTasks
接口的 public 方法实际要调用重载方法 private void loadTasks(boolean forceUpdate, final boolean showLoadingUI)
来实现,使得载入功能达成以下目标:
- 可以选择是否强制载入数据
- 第一次绘制 UI 时必定强制载入
- 对不同清空下的载入操作,可以选择是否显示等待提示
- 强制载入时要显示等待提示
- 清空、标记是否完成、筛选等操作无需显示等待提示
实际的实现代码中要注意:
- 方法需要从远程服务器获取数据,因而测试的时候必须正确设置 Espresso 的 IdlingResource,保证测试等待正确的时长
- 在获取数据后,不再显示等待提示
方法中的显示逻辑部分则被进一步提取为 private 方法,使用 processTasks 来实现,并在其中分辨任务列表是否为空,然后再根据任务的筛选条件显示不同的结果。
openTaskDetails
completeTask、activateTask 和 clearCompletedTasks
这三个方法都需要再操作之后重新刷新页面,显示新的任务列表,但是不需要向远程服务器获取数据,所以需要调用方法 loadTasks(false, false)
TasksActivity
onCreate
页面中有滑动菜单存在,同时其绘制代码较多而且和 fragment 关系不大,所以在 activity 中使用了方法 private void setupDrawerContent(NavigationView navigationView)
实现。
页面需要保存筛选条件的状态,防止在设备配置变更后显示默认的未筛选任务列表。
setupDrawerContent
点击滑动菜单的菜单项会进入任务列表页面或任务统计页面,而且任一页面都会作为返回栈的最底层任务,通过设置 intent 的 flag 来实现:
|
|
最后不要忘记使用 setChecked(true)
把选中的菜单项标记为“已选中”。
TasksFragment
任务列表使用 ListView 实现。而且,需要同时实现空列表页面,然后用 setVisible 方法设置它是否可见。
onCreateView
最重要的是构建 ScrollChildSwipeRefreshLayout 实例的代码,实现了界面元素中 ListView 的刷新功能。
|
|
之后通过回调定义刷新的动作是载入任务列表:
|
|
loadTasks 的参数为 false 保证只有第一次启动页面会强制载入远程数据,之后的下拉动作不会强制载入,只有选择强制载入操作(标题栏菜单 refresh 选项)才会重新获取远程数据。
showFilteringPopUpMenu
使用 PopupMenu 实现弹出菜单,和普通菜单的使用方法相同。通过调用 Presenter 的 setFiltering 方法设置筛选条件,而后通过 Presenter 的 loadTasks(false)
来重新载入任务。
setLoadingIndicator
通过 SwipeRefreshLayout 来实现,但是要注意页面中的 SwipeRefreshLayout 是 fragment 中的元素,所以要使用 getView().findViewById()
方法而不是直接用 findViewById
方法。
而且,方法被调用的情况稍显复杂,可能会出现 getView 返回 null 的情况(比如在 onCreateView 中调用了本方法),此时应当直接结束方法流程。
最后,要保证 UI 绘制完毕之后再显示等待指示的图像,所以要将刷新任务的线程通过 View 的 post 方法传入 UI 线程的消息队列中。
TasksAdapter 和 TaskItemListener
ListView 的列表项需要实现两种点击逻辑:点击选择框和点击任务信息,而选择框又分为完成任务和激活任务两种。为了方便代码管理,使用回调接口 TaskItemListener 定义点击事件。
另外,注意对不同类型的任务设置不同的任务背景色,即在 res/values/colors.xml 中设置:
|
|
而后设置已完成任务的背景为 res/drawable/list_completed_touch_feedback.xml
|
|
设置未完成任务的背景为 res/drawable/touch_feedback.xml
|
|
ScrollChildSwipeRefreshLayout
扩展 SwipeRefreshLayout 的原因可以参考官方文档: {% blockquote https://developer.android.com/reference/android/support/v4/widget/SwipeRefreshLayout.html %} This layout should be made the parent of the view that will be refreshed as a result of the gesture and can only support one direct child. {% endblockquote %} 根据官方文档的说明,SwipeRefreshLayout 只能让它的一个直接子 View 有刷新功能。
但是在这个 app 中 fragment 的界面较复杂,页面示意结构为:
|
|
需要刷新功能的是其中的 ListView,明显它不是 SwipeRefreshLayout 的直接子元素,那么就需要扩展 SwipeRefreshLayout 类,覆盖 canChildScrollUp 方法,并且另外构建一个方法传入子元素的引用来确定刷新的目标。 最重要的代码如下所示:
|
|
这其中 ViewCompat.canScrollVertically
可以用来检查 View 是否能够滚动,而第二个参数 -1 则表示滚动方向为竖直方向。
这样只需要在构造类对象的时候调用 setScrollUpChild 方法,传入特定子元素引用即可,而如果不调用 setScrollUpChild 则和默认的 SwipeRefreshLayout 行为没有任何区别。
参考文章
【Android】不可不知的开发技巧之View.Post()
Android应用启动优化:一种DelayLoad的实现和原理(下篇)](http://androidperformance.com/2015/12/29/Android%E5%BA%94%E7%94%A8%E5%90%AF%E5%8A%A8%E4%BC%98%E5%8C%96-%E4%B8%80%E7%A7%8DDelayLoad%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%92%8C%E5%8E%9F%E7%90%86-%E4%B8%8B%E7%AF%87.html)
StackOverflow Question: Scroll up does not work with SwipeRefreshLayout in Listview