[译]Epoxy:Airbnb的安卓视图架构

原文:link
作者:Eli Hart
项目:github.com/airbnb/epoxy

安卓的RecyclerView是一个显示列表的强大工具,但是它的使用涉及混杂的样式和设置。我们团队有个显示多样式的复杂列表的需求,要有分页、支持平板电脑显示以及item动画。我们发现一直重复同样的模板配置工作,于是我们开发了Epoxy来减缓这种趋势,并且简化静态、动态加载内容的列表类视图的实现。

Epoxy以组件的方式来创建列表,列表中的每个item通过model来定义绘制所需的layout,id和span(间距).model还负责绑定数据到它的view上,并且在view回收时释放资源。这些model按你希望显示的顺序插入到Epoxy的adapter中,adapter来负责复杂的显示工作。

用Epoxy显示搜索结果

我们来通过一个例子来看一下它是如何工作的。这是一个Airbnb中显示城市中邻居的搜索结果页。

我们可以将这个视图拆分为:

  • 一个城市介绍的标题栏
  • 一个城市向导的链接
  • 不定数量的街区视图流
  • 嵌入街区视图流的过滤提示

除此之外有时还有一些其它的视图,比如:

  • 分页的时候结尾部分的加载提示
  • 网络出问题时的错误信息
  • 当滚动完所有结果之后的文字
  • 在某些国家显示的定价声明

总共有8种不同类型的视图,我们需要用RecyclerView来讲他们组合在一起,从而使整个页面可以完整显示并滚动。

为这样一个包含多样式的页面设置RecyclerView的adapter十分操蛋。我们将会有好多乱七八糟的东西:视图类型id,item数量,间距值,view holder,点击事件处理等等。

使用Epoxy的组件方式可以将注意力集中于item需要显示什么,显示适配工作将交由models代理完成。

大概这个样:

public class SearchAdapter extends EpoxyAdapter {
    public void bindSearchData(SearchData data) {
      header.setCity(data.city);
      guidebookRow.showIf(data.hasGuideBook());
      for (Neighborhood neighborhood : data.neighborhoods) {
        addModel(new NeighborhoodCarouselModel(neighborhood));
      }
    loader.showIf(data.hasMoreToLoad());
      notifyModelsChanged();
    }
  }

bindSearchData方法接受一个object类型对象,其中包含了view所需的所有信息。它是幂等的(相同参数多次执行结果相同),当条件发生变化它将重新创建model state来反映新的搜索结果。最后一行,我们告诉Epoxy去执行diff操作,当数据发生变化时将通知到RecyclerView上。

这和React的组件UI非常类似。代码只需描述什么需要被显示,adapter来负责具体的显示工作。我们不需要挨个定义ids,counts,holders等。另外,我们也不需要去处理变化通知的工作。

这使得其成为一个加载多数据源(数据库,缓存,网络请求)的非常优秀的框架。它在adapter中记录一个状态对象,adapter创建models来反映当前状态。当状态对象发生变化时,不论是用户输入或者新数据加载导致的,新的状态将传递给adapter,然后models会再次更新。点击事件可以在model中设置,来回调到Activity。

这种方式职责分离很清晰。models可以根据需求很简单的引入或剔除。通过组件以及adapter的抽象封装。

通常频繁的item变化会十分影响效率,然而,Epoxy加入了一个相当牛逼的算法来检测models的变化,只有在models真正变化时才更新UI(需要确认和google原生DiffUtil的关系)

检测adapter item的变化

一般的adapter中另一个复杂点是跟踪item的变化。item有可能被添加、删除、更新、移动,adapter需要根据这些不同类型的通知去执行相应的处理。这些通知使得RecyclerView可以只重绘变化的views,以及执行相应的动画效果。然而,在已经非常复杂的adapter中手动处理这些问题会十分困难。

Epoxy在models上使用了比对算法来帮你解决这类问题。任何时候当你改变model设置,Epoxy将找出这些变化并反映到RecyclerView上。这将简化你的adapter代码,很容易的添加item变化的动画效果,并且通过只改变需要改变的views来提高性能。

比对算法根据每个model实现的哈希值,所以可以侦测到model的改变,Epoxy提供了注解的方式,所以你的model可以很简单的通过注解来标注需要被考虑进model状态变化的字段。自动生成的子类会帮你实现hashcode计算函数以及各字段的getter和setter。

继续我们上面的例子,header model大概长这样:

public class HeaderModel extends EpoxyModel<HeaderView> {
    @EpoxyAttribute City city;
        
    @Override
    public void bind(HeaderView headerView){
        headerView.setImage(city.getImage());
        headerView.setTitle(city.getName());
        headerView.setDescription(city.getDescription());
    }
        
    @LayoutRes
    public int getDefaultLayout() {
        return R.layout.model_header_view;
    }
}

一个HeaderModel_类会被自动生成,并且包含setCity方法,我们就用这个实例来向models列表中添加一个头部。头部视图只会在city对象变化时刷新。这里假设City对象也有一个hashCode的实现来定义它的状态变化。

你可能注意到了,getDefaultLayout()方法返回了一个layout资源。这个就是用来绑定数据的视图资源,同时这个值也被用作adapter中的视图类型。

Stable IDs By Default

为了让功能正常工作,Epoxy默认启用了RecyclerView的stable id(要了解什么是stable id,参见RecyclerView Adapter的setHasStableIds(boolean hasStableIds)方法)。

这使得item动画以及状态保持得以实现。每一个model都需要定义它的id,我们手动在动态生成的model里设置了一个id。例如,每一个邻居轮播图model的id与网络请求返回的邻居对象相关联。

静态视图,例如我们的头部视图,没有与之关联的id,所以我们得造一个出来。Epoxy会为每一个新创建的model自动生成一个id,并且这个id在app生命周期中确保唯一,负值的id用来避免和手动设置的id相冲突。

唯一需要注意的是,我们必须在adapter生命周期中使用同一个model。对于头部视图(或其他静态视图)这就代表我们定义了一个final类型的字段,并在内联方法中将其初始化。然后将其加入model列表并且像通常一样去更新。我们不必再去做更多工作来保证其唯一。

保存视图状态

Epoxy也加入了对视图列表的状态支持,RecyclerView默认并不支持。例如,上图中的轮播图可以左右滑动,为了更好的用户体验,我们希望保存其滑动位置。当用户往下滑动然后再返回时,轮播图应该停在之前相同的位置,同样当用户旋转屏幕或者切换到其他app再切换回来,尽管Activity已经重新创建,我们也应该展现相同的页面状态。

在一般的RecyclerView adapter中实现要费了老劲了,然而,Epoxy直接就支持,啥model都能保存状态。它是通过leveraging stable ids将model id与视图的序列化状态相关联来实现的。

使用也很简单:

@Override
public boolean shouldSaveViewState {
    return true;
}

默认状态为false,来减少对性能的影响。

Epoxy关于静态内容的使用

RecyclerView经常被用来显示远程加载的动态内容。其他的内容一般用Scrollview来实现,使用Epoxy可以在不用做过多工作的情况下使用RecyclerView来替换Scrollview。下面的列表就是这么用的:


使用ScrollView实现最为简单,但是使用Epoxy的RecyclerView可以有更快的加载速度,添加动画效果也更简单。

这个页面的性能问题非常重要,因为用户点击搜索结果时就会跳转到这个页面,让这个过渡动画效果平滑是提升用户搜索体验的重要因素,同时还需要详情页加载非常快才行。

让我们来看一下详情页上的元素是如何影响性能的。首先,头部的照片是一个横向的RecyclerView,中间有一个静态地图显示地理位置,底部还有一个横向RecyclerView显示附近类似房子,中间还有一些描述性文字和小图片。

整体上构成了一个十分复杂的视图结构,包含了很多bitmaps。这使得测量和展现时间变得很长,并且需要更多的内存去加载图片。

另外,我们通过多种渠道加载数据——数据库,内存缓存,多个网络请求——来支撑这个页面,这对向用户显示即时信息很有帮助,但如果处理不好则需要花费额外的时间去更新视图。

综上所述,我们必须要关心效率问题。谢天谢地有了Epoxy!让我们有了极致的用户体验!!(妈了个鸡。。。我为啥要翻译这个。。。)

  • 因为使用了RecyclerView,当用户首次加载时,只有一小部分视图需要加载。这避免了过早加载地图视图和底部的轮播图,以及中间的那些玩意儿。于是有了更快的加载速度,更小的内存使用,以及最为关键的梗平滑的过渡效果。
  • 防止了上下滑动时视图结构失效引起的帧率下降,假如有数据返回但对应的view不在屏幕上,我们都不用管。如果时间字段变化导致价格需要更新,Epoxy会自动更新价格。
  • item变化动画效果。数据变化时我们可以用平滑的动画效果来隐藏、显示或更新视图。例如,点击翻译会插入一个loader动画,当翻译完成时会自动变为翻译后的结果,避免了默认的不自然的变化效果。

Epoxy的未来

开源了, 欢迎贡献~我们会持续改进注解处理,对比算法,以及工具类。欢迎引入其他牛逼的库。

Check out all of our open source projects over at airbnb.io and follow us on Twitter: @AirbnbEng + @AirbnbData

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容