Epoxy : 简化 RecyclerView Adapter 的小工具介绍

作者: rain 分类: 移动 发布时间: 2016-12-13 23:39 6 条评论

RecyclerView 是 Android 中用来显示列表的常用工具。如果要显示的列表项有各种复杂的类型、还要支持翻页加载、支持平板界面以及支持每个条目的动画效果,则需要大量的模板代码和配置项才能实现这些功能。使用的次数多了,你会发现需要不停的复制这些配置代码。 Airbnb 的工程师们 就遇到了这种情况。所他们就开发了 Epoxy 这个工具用解决他们在使用 RecyclerView 中遇到的问题。
Epoxy 使用组合的方式来构建一个列表。列表中的每个项目都通过一个 model 对象来定义,里面包含了每个项目的 布局文件、id以及所占的空间(span)。Model 还实现了把数据设置到每个 view 的功能、以及被回收的时候需要释放 view 所使用的资源。 Model 按照显示的顺序添加到 Epoxy 的 adapter 中, adapter 负责处理显示每个项目。

使用 Epoxy 来显示搜索结果

通过实际的示例来看看具体是如何使用的。下图是 Airbnb 应用的搜索结果界面。

image

这个界面包含如下内容:
* 一个标题项目用来介绍城市
* 一个跳转到城市手册的链接
* 一个显示附近邻居的模块,显示的数目是不确定的
* 一个搜索建议的提示模块

除此之外,在搜索过程中,不同的情况还会显示其他一些模块:

  • 当滑动到页面底部去加载下一页内容的时候,会显示一个加载中 view
  • 如果遇到网络错误,会显示一个错误信息 view
  • 当查看了所有搜索结果内容的时候,会显示一个 文本提示信息 view
  • 在一些国家会显示一个价格免责声明 view

这样的话,这个搜索结果界面就有 8 种类型的 view 类型, 我们使用 RecyclerView 来显示这些类型从而保持使用统一的类型来显示这个界面。

正常情况下,在 RecyclerView adapter 中添加这么多 view 类型会很复杂。需要一个复杂的类来指定 view 的类型、view 的数量、每个 view 所占用的空间(span)、 view holder、click listener 等等。

如果使用 Epoxy 的话,组合的方式可以告诉 adapter 如何显示每个 view item,具体显示的细节则可以通过 model 来实现。

代码差不多看起来如下:

bindSearchData() 函数的参数包含了需要显示这个列表的所有数据。每当数据发生了变化,就会调用这个函数,然后重新创建当前的 model 状态来反映新的数据。最后一行代码,告诉 Epoxy model 变化了, Epoxy 会计算新的 model 和旧的 model 之间的不同,然后通知 RecyclerView 具体发生了哪些变化(添加、删除、修改了 item 等变化)。

这个 JavaScript 框架 React 的方式类似。代码只需要指定需要显示什么, adapter 负责处理如何显示每个 view item 的细节。我们无需在 adapter 中定义任何确定的信息,比如 item ids、数量或者 view holder。 甚至,也无需告诉 RecyclerView 哪些数据变化了。 Epoxy adapter 会处理好这些。

这样就可以构建一个优美的架构,Activity 从不同数据源加载数据(数据库、缓存、或者网络)。把这些数据保存到一个数据对象中并使用该对象更新 adapter,adapter 使用新的数据对象来重新创建当前的 model 。只要数据变化了,adapter 就接收到新的数据并重新构建 model。点击事件可以在 model 中设置并回调到 Activity 中。

这样的话,责任就被明显的区分开了。当设计发生了变化或者添加的新功能,则 model 可以很方便的添加和删除。由于 adapter 提供的组合模式和抽象,复杂度降低了。

大部分情况下,由于 adapter item 经常更新会导致性能问题。但是,Epoxy 实现了一个内部的 diff 算法来检查 model 之间的改变,只更新需要更新的 model。 这样可以解决性能问题。

追踪 adapter item 的变化

Adapter 追踪 item 变化是一个复杂的任务。item 可以被添加、删除、更新或者移动了位置, 而 adapter 必须正确的通知每个 item 的这些变化。正确的通知每个 item 的状态变化,则可以提高 RecyclerView 的显示效率,这样 RecyclerView 只更新需要更新的内容。但是在一个已经比较复杂的 Adapter 中去实现这个复杂的功能是比较困难的。 Android support 库中还包含了一个 SortedList 工具类来帮助实现这个功能。

Epoxy 通过在每个 model 上使用 diff 算法来解决这个问题。每当你修改了一个 model 的时候, Epoxy 就使用 diff 算法找到 model 直接的变化,然后通知 RecyclerView 这些变化。这样您的 Adapter 代码就比较简单了,自动提供了 item 变化的动画,并且只在必要的时候去重新绑定更新的 view 从而提供的性能。

Diff 算法的正确使用离不开 model 的 hashCode,所以你需要正确的实现这个函数。为了简化 model 的编码,Epoxy 提供了注解处理器( annotation processor ),这样你的 model 只需要使用注解每个代表状态的 field 即可。注解处理器会生成一个子类来实现 hashCode 函数,并且还实现了每个 field 的 getter 和 setter 函数。

同样还是继续使用上面的示例:

上面的 model 定义将会自动生成一个 HeaderModel_ 类,该类还有一个 setCity 函数,当需要把上面的 model 添加到 Adapter 中,需要使用 HeaderModel_ 这个生成的对象。这样的话,只有当 City 对象发生了变化,才会导致 header view 更新。上面假设 City 对象也正确实现了 hashCode 来表示其状态。

上面的 model 还实现了 getDefaultLayout() 来返回一个 layout 资源。该 layout 资源用来创建 model 对应的 view 并在 model 的 bind 函数设置数据到 view 中用来显示。同时该 layout 的 id 也是这个 model 在 Adapter 中的 view type id。

Stable IDs By Default

Epoxy 需要启用 stable id 才能正确使用。这样 diff 算法、item 动画和状态保存才能正确实现。每个 model 都需要定义一个 id,如果没有定义,则 Epoxy 会动态的生成一个。 例如 显示附近邻居的结果 model 使用 neighborhood 对象中的 id 来作为 model 的id。

像 header view 这种静态的 view 比较麻烦一点。这些静态的 view 没有 id 和 model 关联,所以 Epoxy 需要为他们制定一个。Epoxy 为每个新的 model 自动生成一个 id。这个 id 在整个应用的生命周期中是唯一的,并且使用负数为 id 来避免和您手工设置的 id 重复。

唯一需要注意的是,在一个 Adapter 的生命周期中我们需要使用同一个 model。对于上面的 header model(静态的 view),需要定义个 final 类型的 model 对象,并初始化这个对象,然后在需要的时候把该对象添加到 Adapter 中,根据需要来更新 model 的数据。

保存 View State

Epoxy 还支持保存列表中每个 view 的状态,RecyclerView 现在默认还不支持该功能。例如,上面的邻居搜索列表也是一个水平滚动的 RecyclerView,在上下滚动搜索结果的时候, 保存 邻居水平 RecyclerView 滚动的位置是一个好的用户体验。用户查看到第五个位置,然后上下滚动查看其它信息再次滚动回去后还能显示用户之前看到的状态。同样,如果用户旋转了手机和平板的屏幕方向,还是能保持同样的滚动位置。

要使用该功能,只需要添加下面一个函数即可:

当你的 model 不可见的时候,Epoxy 会保存其状态,当 model 再次显示的时候恢复其状态。默认情况下是没有保存 view 状态的,这样是为了提高滚动效率(避免保存一些不必要的 view 状态导致滚动卡顿)。

把 Epoxy 用于静态页面

RecyclerView 通常用于显示动态的列表内容,比如从服务器查询的房源信息列表。对于静态内容大部分情况下使用一个 ScrollView 就可以了。如果用 Epoxy 的话, 静态页面也可以使用 RecyclerView 并且工作量和使用 ScrollView 并没有太大差别。例如下图的房源详情界面:

image

这个界面可以使用 ScrollView 来实现。但是使用 Epoxy 可以提高界面的加载效率,并且实现动画也增加简单。

对于 Airbnb 来说,这个界面的加载效率是非常重要的,由于用户经常查看搜索的房源信息。用户点击一个搜索结果,使用一个 shared element transition 来把房源图片从搜索结果界面转换到房源详情界面。这个动画的流畅性决定了用户体验是否足够好,只有房源详情界面加载速度非常快的情况下,这个动画才能足够流畅。

来看看详情界面的布局了解下该界面对性能的影响有多大。首先,上面的 图片其实是一个水平的 RecyclerView ,这样用户可以查看房源的多张图片。中间有个静态的地图显示房源的位置,而底部有另外一个 RecyclerView 来显示附近的其他房源信息。中间还穿插一些其他文本和图片信息来详细的描述当前的房源。

这个界面的 view 层级比较复杂并且需要显示多个图片。这样计算这个界面的布局信息(measure and layout )就需要消耗比较多的时间,同时还需要更多的内容来加载图片。

另外我们还从各种数据源加载数据,数据库、缓存、多次网络请求 来提供这个界面需要的信息。立刻显示用户信息是非常重要的,但是如果处理不好,则填充这些数据需要很多时间。

由于复杂的 view 层级、很多图片需要显示以及需要刷新多个 view 来显示新的数据,所以这个界面性能问题非常明显。使用 Epoxy 则可以提供下面三个好处:

  • 由于使用 RecyclerView ,当用户打开详情界面的时候只需要初始化一部分用户看到的界面。这样就避免提前初始化地图以及下面的附近房源信息,以及他们之间的其他 view。这样该界面加载速度显著提高、占用更少的内存从而提供更流畅的界面切换动画。
  • 当用户滚动界面的时候,不需要不停的从新计算 view 的布局信息,实现滚动的流畅性。如果请求到新的附近房源信息,但是现实房源的 RecyclerView 当前不可见,则 view 不需要做任何更新。如果总价变化了,则 Epoxy只需要更新价格这个 TextView 即可。
  • Item 变化自动带有动画。数据变化可以隐藏、现实或者更新对应的 view 并且使用流畅的动画完成这些 view 的状态变化。例如,点击翻译评论的按钮,会出现一个加载中的 view,当结果返回的时候,自动使用动画来显示新的内容。这样可以避免 view 状态变化带来的界面卡顿。

Epoxy 未来展望

Epoxy 是 Airbnb 开发团队新开源的一个库,Airbnb 欢迎感兴趣的工程师和他们一起来逐步完善这个小小的工具库。

本文出自 云在千峰,转载时请注明出处及相应链接。

本文永久链接: http://blog.chengyunfeng.com/?p=1025

Ɣ回顶部