本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Epoxy Controller
哲学理念
EpoxyController鼓励使用流行的Model-View-ViewModel和Model-View-Intent模型.
单向数据流: 从自定义的state, 到EpoxyModel, 再到RecyclerView上面的视图.
用户对视图的输入触发了回调, 可能会更新state以及重启单向循环.
Epoxy支配了state和EpoxyModel之间以及EpoxyModel和视图之间的接口. 如何管理状态和视图回调取决于开发者.
EpoxyModel相似于ViewModel. 它是不可变类, 是数据和视图之间的接口. 它格式化了数据和相应地更新了视图. 视图不能修改EpoxyModel, 且必须进行回调以修改state, 由此新的EpoxyModel便产生了.
这不同于EpoxyAdapter, 因为EpoxyModel是可变的且能够被更新和重用. EpoxyAdapter模式是不可预测且倾向于出错的, 这在EpoxyController中是不被允许的.
使用
继承于EpoxyController且实现了buildModels方法. 该方法的目的是构建EpoxyModel, 它在方法被调用时表示了数据的状态. 由此产生的Model是不可变的, 并被Epoxy用于在RecyclerView中创建和绑定视图. 在buildModels再次被调用时, Epoxy diff出了最新的Model与之前Model之间的不同, 来决定RecyclerView需要进行哪些更新.
自定义buildModels实现应该创建新的EpoxyModel实例(或者使用AutoModel), 在Model上设置恰当的数据, 然后将它们依照在RecyclerView上展示的顺序添加进来. 这些数据也可以通过调用EpoxyController#add(EpoxyModel)或者EpoxyModel#addTo(EpoxyController)进行添加.
使用Controller的时候, 先创建实例然后调用getAdapter()来获取用在RecyclerView上面的后备Adapter. 然后调用Controller的requestModelBuild函数告知Epoxy触发模型重建并更新Adapter(不能直接调用buildModels函数). 在数据变化的时候再次调用requestModelBuild, 你将开发地看到RecyclerView进行相应的更新.
示例
public class PhotoController extends EpoxyController {
private List<Photo> photos = Collections.emptyList();
private boolean loadingMore = true;
public void setLoadingMore(boolean loadingMore) {
this.loadingMore = loadingMore;
requestModelBuild();
}
public void setPhotos(List<Photo> photos) {
this.photos = photos;
requestModelBuild();
}
@Override
protected void buildModels() {
new HeaderModel_()
.id("header model")
.title("My Photos")
.addTo(this);
for (Photo photo : photos) {
new PhotoModel_()
.id(photo.getId())
.url(photo.getUrl())
.comment(photo.getComment())
.addTo(this);
}
new LoadingModel_()
.id("loading model")
.addIf(loadingMore, this);
}
}
Controller像这样设置:
controller = new PhotoController();
recyclerView.setAdapter(controller.getAdapter());
controller.requestModelBuild();
之后再调用几次网络请求来加载图片, 我们会调用controller.setPhotos来更新图片列表controller.setLoading(false)来停止加载更多.
注意: 这些setter都调用了requestModelBuild通知Epoxy重新构建Model. 在Controller内部还是外部调用这个函数是自己的选择. 相似地, 这个例子里面将数据存为字段并使用setter方法进行更新, 但EpoxyController没有要求buildModels中使用的数据应该如何存储或者访问. 这种灵活性使得EpoxyController适配于任何遵循的架构. 然而, 如果想要更加结构化, 则可以使用TypedEpoxyController
Model构建细节
为使Controller正确地工作, Epoxy期待Model遵从2个重要规则:
- 所有Model必须设置唯一id.
- Model是不可变的并且它们的状态一旦添加进Controller便不可再改变. (唯一的例外是Interceptors)
这些规则对于diff正常工作是必要的, 由此每一个视图保持了与数据状态的一致性. Epoxy通过运行时的验证强制保证了这些规则.
Model在实例化的时候不允许赋值默认id. 这只在EpoxyAdapter中允许. EpoxyController中的每一个Model必须设置显式的id, 或者是注解AutoModel.
requestModelBuild做了它的名字显示的事件, 它要求Model会构建, 但不保证马上构建. 只有在Controller上面的首次构建是立即的(由此视图会立即填充并且视图状态能够存储), 但是后续的调用是基于post且防抖动的. 这是为了从数据变更中解耦Model构建. 基于这种方式, 所有数据更新可以完全完成而不必担心调用requestModelBuild多次.
比如, 在PhotoController例子中, 如果同时修改了加载状态和照片列表, 我们不必触发2次Model构建. 有了post和防抖, 我们能够安全地调用requestModelBuild2次(每个setter调用一次), 而Model则只会构建一次. 这将减轻调用代码对调用requestModelBuild函数的优化尝试.
每次buildModels调用是完全独立于之前的调用的. buildModels总是以空Model列表开始并且必须创建, 修改和添加全部彼时表示数据的Model.
当然也可以通过使用EpoxyModel#addIf而非常规的add来有条件地添加Model. 在EpoxyController中, 通过EpoxyModel#hide()隐藏Model是不被允许的.
Adapter和diffing详情
一旦Model被创建, Epoxy便在后备Adapter上设置了新Model并执行了diffing算法来计算与之前Model列表的变更. 任何一项的变更都会通知给RecyclerView, 由此视图能够按需删除, 插入, 移动和更新.
Epoxy管理了所有视图的创建, 绑定和复用. 视图绑定的调用委托给了Model, 所以Model能够使用它所代表的数据更新视图. 其它的视图生命周期事件, 例如绑定/解绑Window和视图回收, 也被委托给Model.
异步支持
EpoxyController还有个构建函数接收2个Handler, 一个用于运行Model构建, 一个用于处理diffing. 默认情况下这些操作运行在主线程, 但是基于性能改善的考虑, 这些操作也允许异步处理.
注意: EpoxyController首次构建Model总是运行在主线程上, 即使设置了特定的异步Handler. 首次创建视图时允许视图同步恢复保存的状态是很必要的.
要想开箱即用异步操作, 可以通过AsyncEpoxyController实现. 如果偏向于控制Epoxy使用的线程, 往下读吧.
默认的Handler能够通过设置静态字段EpoxyController#defaultDiffingHandler和EpoxyController#defaultModelBuildingHandler进行修改, 两者强制应用中的全部Controller使用异步行为.
比如, 像这样设置全局执行异步行为:
HandlerThread handlerThread = new HandlerThread("epoxy");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
EpoxyController.defaultDiffingHandler = handler;
EpoxyController.defaultModelBuildingHandler = handler;
Epoxy在类EpoxyAsyncUtil中提供了一些通用工具, 用于创建异步Handler. 比如, 可以使用函数EpoxyAsyncUtil#getAsyncBackgroundHandler很容易地获取Handler, 而这个Handler使用了专用的异步后台线程.
使用Handler是因为Epoxy需要像默认一样能够很容易地工作在主线程. 而且, Epoxy并不支持并行Model构建, Handler强制使用单线程轮询机制.
如果使用异步Model构建, 必须要保证数据访问是线程安全的 - Epoxy并不会保证这些. 这意味着自定义EpoxyController#buildModels在所有的数据访问下都必须是线程安全的.
如果Model构建和diffing是提供的相同的Handler, 那么所有的工作将在相同的线程中同步执行, 从Model构建开始到diffing结束.
监听Model变更
如果想要在Model完成构建和diffing, 以及RecyclerView进行了更新时获得通知, 可以使用EpoxyController#addModelBuildListener函数来注册回调.
使用此选项可以对通知RecyclerView后需要发生的模型更改做出反应, 例如滚动.
Validations
上面提到, EpoxyController期待每一个Model拥有唯一的id并且在添加到Controller之后不被修改(例外是Interceptors). 违反这个将导致RuntimeException.
这些检验极其重要, 它能够在使用Epoxy时帮助避免错误. 检验默认是开启的, 但也可以禁用. 可以在生产环境时禁用来以避免验证检查的运行时开销.
检验的主要目的是强制执行Model的不可变性. 任何会导致Model的hashCode发生变更的操作都是不允许. 这些错误很容易犯, 因为Model并不拥有编译器强制执行的不可变性; 也就是说, EpoxyModel的通用模型并没有使用final字段和构建者模式, 所以编译器将依然允许字段发生改变. 运行时检验就地向你警告任何可能的意外违规, 通过周期性在断言每一个Model的hashCode与首次添加时是否发生变化.
AutoModel
若Model总是存在于Adapter(比如header或者loader), 则可以将它们使用@AutoModel标记为字段, 由此Epoxy将自动地创建Model并赋值唯一id. 这个id即使在不同的Adapter中也依然是稳定的, 所以它能够用于在配置变更时保存Model状态.
注意: AutoModel在PagedListEpoxyController并不起作用, 并且如果在使用Kotlin的话, 也不推荐使用. 对于推荐的Kotlin的用法请查看使用Kotlin
比如, 可以像这样更新PhotoController:
public class PhotoController extends EpoxyController {
@AutoModel HeaderModel_ header;
@AutoModel LoadingModel_ loader;
private List<Photo> photos = Collections.emptyList();
private boolean loadingMore = true;
public void setLoadingMore(boolean loadingMore) {
this.loadingMore = loadingMore;
requestModelBuild();
}
public void setPhotos(List<Photo> photos) {
this.photos = photos;
requestModelBuild();
}
@Override
protected void buildModels() {
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);
}
}
AutoModel注解的字段是自动创建的; 在每一次buildModels调用之前, 新的实例会被创建. 由此, 对于AutoModel注解的字段不要手动进行赋值, 也不要在buildModels函数之外对字段进行修改. 而且, 请记住:因为每一个Model总是重新会创建的, 所以不能使用==跟Adapter中的Model比较字段值(比如在onBind回调中).
生成的id总是负值, 所以它们不太可能与手动设置的id冲突(比如来自数据库的id通常大于0).
隐式添加
(在2.1以后可用)
如果隐式添加启用的话, AutoModel注解地Model在buildModels函数内修改之后会自动地创建并添加到Controller里面. 这允许了在构建Model之后遗漏掉addTo函数. 隐式添加默认是禁用的.
有几个规则主导了这个进程. 首先, AutoModel对于隐式添加是"staged", 一旦在buildModels函数内修改(例如Model上的任何setter被调用来更新数据). 一旦Model变成staged, 它将自动添加到Controller, 一旦其它的Model被修改或者添加(或者buildModels函数返回了).
如果Model在addIf条件下失败, Model将会从staging状态移除. 如果staged Model手动地通过addIf, addTo,或者add(...)添加, 那么它将从staging中删除, 不会被添加两次.
如果不需要修改Model(比如loader), 那么隐式添加将没有方法来stage这个Model, 所以必须像常规方式一样手动添加 - model.addTo(...)或者add(model).
隐式添加可以是漂亮的删除模板代码的方式, 如果有许多AutoModel的话. 下面是一个例子, 展示了如何使用隐式添加:
@Override
protected void buildModels() {
header
.title("My Photos");
// No "addTo" call is needed here. The model will automatically be added.
for (Photo photo : photos) {
new PhotoModel_()
.id(photo.getId())
.url(photo.getUrl())
.comment(photo.getComment())
.addTo(this);
}
loader
.addIf(loadingMore, this);
}
使用内部类
AutoModel注解对内部类类型的字段并不起作用 (静态嵌套类是可以的). 如果要用EpoxyController作嵌套类 (由此可以访问外部类的数据), 那么必须手动处理所有的id. 好的使用模型如下:
@Override
protected void buildModels(){
new HeaderModel_()
id("header")
.title(..)
.subtitle(...)
.addTo(this);
...
new LoaderModel_()
.id("loader")
.addIf(isLoading);
}
这和@AutoModel基本一样, 但是通常而言, 我们偏向于使用AutoModel, 这样便不必担心id冲突或者赋值唯一id. 简单的非嵌套Controller中你可能依然喜欢这种方式.