本文已参与「新人创作礼」活动,一起开启掘金创作之路。
与Kotlin一起使用
如果在用Kotlin写EpoxyController类, Epoxy会产生扩展函数来构建Model. 这取代了AutoModel模型.
对于HeaderViewModel, 它的使用像这样.
override fun buildModels() {
headerView {
id("header")
title("Hello World")
}
}
要使用的话, 请确保build.gradle文件中使用了kapt插件(apply plugin: 'kotlin-kapt'). 之后Epoxy会在每一个包含生成EpoxyModel的包里面生成叫EpoxyModelKotlinExtensions.kt的Kotlin文件. 对包里面的每一个生成的Model, 这个文件会包含EpoxyController类上的扩展函数. 函数名是Model的名字去除了Epoxy或者Model后缀.
这些函数只能用在Controller的buildModels函数里面. 每一个函数接收一个lambda作为参数并以Model作为接收器, 创建新的Model实例, 调用lambda实例化Model, 之后再将Model添加到Controller里面.
每一个这种方式构建的Model必须手动指定id.
Typed Controllers
常规的EpoxyController并没有规定用于构建Model的数据来源于哪. 这就比较灵活, 但也要求在传递和存储数据上面有额外的开销, 或者强制执行了不好的架构模型.
TypedEpoxyController目的在于修复这些问题, 通过删除数据流上的模板代码并强制buildModels成为纯函数. TypedEpoxyController的子类被赋予了数据类型, 而且在Model应该重建的任何时候, 调用setData来传递指定类型的对象. 最后, 使用那种类型的数据对象来调用buildModels方法.
继续使用PhotoController的例子, 可以使用TypedEpoxyController来大大简化代码(但没有loader):
class PhotoController extends TypedEpoxyController<List<Photo>> {
@AutoModel HeaderModel_ header;
@Override
protected void buildModels(List<Photo> photos) {
header
.title("My Photos")
.addTo(this);
for (Photo photo : photos) {
new PhotoModel_()
.id(photo.getId())
.url(photo.getUrl())
.comment(photo.getComment())
.addTo(this);
}
}
}
要使用这个, 那么无论任何时候照片数据发生改变, 或者想要Model重建, 就得调用photoController.setData(photos). 理想情况下, buildModels(List<Photo>)的实现以这种方式来写, 它只依赖于photos输入并且没有负作用, 而非依赖于添加Model. 这种方式是极其可预测, 可读和可测试的.
setData方法取代了常规的基于EpoxyController的requestModelBuild方法. 直接在TypedEpoxyController调用requestModelBuild方法是错误的.
其它可用的类有Typed2EpoxyController, Typed3EpoxyController, 和Typed4EpoxyController - 它们有不同数目的类型参数. 如果数据被多于一个对象表示时, 这将十分有用.
在PhotoController中使用Typed2EpoxyController来保存正在加载的数据:
class PhotoController extends Typed2EpoxyController<List<Photo>, Boolean> {
@AutoModel HeaderModel_ header;
@AutoModel LoadingModel_ loader;
@Override
protected void buildModels(List<Photo> photos, Boolean loadingMore) {
header
.title("My Photos")
.addTo(this);
for (Photo photo : photos) {
new PhotoModel_()
.id(photo.getId())
.url(photo.getUrl())
.comment(photo.getComment())
.addTo(this);
}
loader
.addIf(loadingMore, this);
}
}
在上面的例子中, 调用了photoController.setData(photos, isLoadingMore)将状态包含了是否有更多数据将被加载. 不幸的是, 基础数据类型并不被支持, 所以必须使用Boolean而不是boolean. 另一个缺点是, 默认情况下setData的方法签名显示泛化参数名, 而这是不太清晰明了的.
要修复这个不清晰的问题, 就得覆盖setData方法来提供清晰度.
@Override
public void setData(List<Photo> photos, Boolean loadingMore) {
super.setData(photos, Predicates.notNull(loadingMore));
}
这给予调用者Boolean参数表示的信息, 也防止了null值.
Debug Logs
调用setDebugLoggingEnabled(true)来启动向Logcat打印调试消息的功能. 日志Tag会是Controller的名字.
这些调试消息将包括:
- Diffing结果, 比如哪些项变更被探测并通知. 检验是很有用的, 比如数据变更使得目标回收视图进行了更新. 差异也许会提示了Model状态的不正确设置, Model Id的问题, 或者其它自定义
buildModels时的常见bug. - Timing 输出报告了构建和diff模型花费了多少时间. 这在剖析性能和观察衰退和变慢时十分有用.
过滤重复项
默认情况下, DiffUtildiffing算法忽略具有重复id的项, 这种情况下的行为未定义, 因为它破坏了EpoxyController正常工作所需的RecyclerView稳定id契约. 在复杂的Controller中, 很难保证没有稳定的id回归, 如果有, 我们应该在生产中优雅地恢复.
要过滤重复项, 请调用setFilterDuplicates(true). 启用的时候, Epoxy将会在buildModels期间搜索已添加的具有相同ID的Model并且删除找到的重复项. 如果具有相同ID的Model被找到, 第一个Model将留在Adapter中, 后续的Model将被删除. 在重复项被删除的时候, 它的onExceptionSwallowed将被调用.
如果Model是通过服务器提供的数据创建的话, 过滤重复项将非常有用, 在这种情况下, 服务器也许会错误地发送重复项. 要不然, 如果Model的id依赖于hashing(例如字符串id或者多个数字id), 将有极小的可能性会发生原本想要避免的哈希冲突.
拦截器
通常而言, EpoxyModel在buildModels函数内添加到Controller之后是不可能再修改的. 其中的一个例外是使用了Interceptor. Interceptors在buildModels被调用之后, 且在Model被diff和设置到Adapter之前运行. 那时, 在buildModels期间, 在Model列表添加的时候, Interceptor的intercept函数被调用, 而且Interceptor可以不受限制地修改Model列表和列表中的每一项.
这对于必须对模型进行聚合操作的情况非常有用, 例如切换分隔符. 另一个有用的例子是修改A/B实验的模型.
使用addInterceptor添加Interceptor. Interceptor按照它们被添加的顺序执行, 后续的Interceptor接收前面的Interceptor修改过的Model列表.
对于Interceptor而言, intercept函数返回之后再继续修改Model列表或者列表中的Model, 都是错误的.
吞没异常
如果Epoxy遭遇了可恢复的错误, 它将调用onExceptionSwallowed, 然后尽可能继续运行. 这种情况的通用场景是: 启用setFilterDuplicates时, 重复项出现了.
默认情况下, onExceptionSwallowed什么也不做, 但子类可以覆盖这个函数来提醒出现的问题. 良好的模型应该是在应用内定义基础的EpoxyController, 然后其它的Controller继承它. 在这种情况下, 可以覆盖Controller的onExceptionSwallowed然后在调试模式下抛出异常或者在生产模式下写入崩溃报告. 推荐不要忽略掉onExceptionSwallowed, 因为它报告了Controller中真实的问题.