本文已参加「新人创作礼」活动,一起敞开创作之路。

防止内存走漏

假如不同的RecyclerView运用相同的 Adapter 的话, 有 2 种可能的内存走漏. 一个常见场景是在Fragment的onCreate办法中创立和保存 Adapter 字段, 而且在跨过多个视图创立/毁掉周期内重用它, 假如 Fragment 放入了backstack或者它的实例经过屏幕旋转残留了.

子视图

为了允许状况保存, Epoxy持有了每一个绑定视图. 为了防止这些视图走漏, 仅仅确保RecyclerView在用完视图之后将它们彻底收回. 一种方式是经过recyclerView.setAdapter(null)办法将 Adapter 从RecyclerView中解绑(很可能是在Fragment的onDestroyView办法里面).

这种方式的缺陷是视图会立刻整理掉, 所以假如要离开屏幕上做动画, 在动画完成之前屏幕会呈现空白. 要防止空白的更好的选项是在RecyclerView从窗口中解绑时, 用LayoutManager收回子视图.假如启用了setRecycleChildrenOnDetach(true)的话, LinearLayoutManagerGridLayoutManager会主动地进行回调.

要到达主动收回的意图, 能够在项目中创立一个承继自EpoxyAdapterBaseAdapter.

public class BaseAdapter extends EpoxyAdapter {
        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            // This will force all models to be unbound and their views recycled once the RecyclerView is no longer in use. We need this so resources
            // are properly released, listeners are detached, and views can be returned to view pools (if applicable).
            if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
                ((LinearLayoutManager) recyclerView.getLayoutManager()).setRecycleChildrenOnDetach(true);
            }
        }
    }

父视图

别的, 和子视图很类似, 指向RecyclerView本身的引证的走漏. 这个场景关于一切的RecyclerView Adapter 是固有的, 而不仅是Epoxy.

发生的场景与上面的相同, 即在RecyclerView毁掉之后, Adapter 被保留了下来. 当RecyclerView设置 Adapter 的时分, RecyclerView注册了个Observer 监听数据项的改变(adapter.registerAdapterDataObserver(...)). 关于RecyclerView而言, 很有必要知道 Adapter 数据项发生改变的时间.

Observer 只要在 Adapter 从RecyclerView上解绑的时分被移除(例如recyclerView.setAdapter(null)). 有了 Fragment 中重建视图的常见模型, 要想不这么做很简单.

一个防止 RecyclerView 走漏的选项是在RecyclerView毁掉的时分解绑它的 Adapter. 但它有上面说到的缺陷, 就是会立即整理视图.

另一个选项是清除Adapter的引证而且每次创立新的RecyclerView的时分从头创立一个Adapter的引证.

最后的一个选项是自定义RecyclerView子类, 当从窗口上解绑时移除自己的Adapter. 而关于RecyclerView内部嵌套的RecyclerView(例如轮播), 就不要解绑Adapter了. 示例app中的轮播代码展现了一种办理绑定和解绑嵌套轮播的更好的方式.

class MyRecyclerView extends RecyclerView {
  @Override protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
        setAdapter(null);
        // Or use swapAdapter(null, true) so that the existing views are recycled to the view pool
  }
}

DoNotHash

Epoxy有Do Not Hash的概念, 即即使hashcode改变了, 也不要更新特点(与惯例特点相反, 它们不管任何时分hash发生改变, 都会从头绑定). 这是个可选的参数, 用于注解EpoxyAttributeModelProp.

DoNotHash是功能优化. 它的预期运用是回调(例如点击监听器), 回调通常是匿名的, 每一个Model构建, 都会有不同的hashcode. 没有“Do Not Hash”的话, 每一次Model进行构建, 差异器都会将具有点击监听器的Model识别为发生改变, 这将发生许多许多Model. 之后这些Model会全部重绑到Recyclerview.

通常这工作得很好, 但也有个很大的问题. 假如回调在它的闭包内捕获了任何变量, 那么这些变量在回调触发时可能会过期.

举个比如, 想要对象Animal用于创立一个EpoxyModel, 之后Model上的点击监听器持有了Animal的引证用于回调内. 假如Animal对象发生了变更, Model发生了重建, 新的点击监听器(持有新的Animal对象的引证)将不会绑定到视图上, 当点击触发的时分, 老的(且不正确的)列表将会用在回调内.

通常这种情况只要在数据是可变的情况下是个问题.

若不需求, 就不要运用DoNotHash

假如你不担心从头绑定点击监听器影响功能, 那就不要添加DoNotHash选项 这也意味着防止运用@CallbackProp, 因为它内部运用了DoNotHash. 这也许是个彻底适宜的计划, 也是最简单的计划.

留意: 假如在运用数据绑定,DoNotHash关于未完成equals和hashcode类型的变量是默许敞开的. 检查数据绑定文件获取更多信息来修改默许设置.

Epoxy的解决计划

要坚持DoNotHash的优点的一起也防止操作的复杂性, Epoxy供给了OnModelClickListener接口取代惯例的View.OnClickListener. 这个监听器供给了EpoxyModel, View, 和点击的适配器的方位. Epoxy生成的代码特别处理了点击监听, 保证了点击时供给最新的EpoxyModel.

假如全部数据保存在Model中, 那么数据能够在onClick中检索到, 而且总是最新的. 你也许需求在Model中存储候选数据以用于点击回调.

这种方式的缺陷是它只对点击监听器有用, 其它任何类型的回调不能充沛运用它.

重要: 假如在点击监听器中捕获的数据发生了改变, 但是Model中的其它特点没有改变, Model依然不会从头绑定, 因为Epoxy并不知道什么东西发生了改变. 请确保一切表示状况的数据在Model中捕获, 而且一切的特点正确在完成了equals和hashcode.

候选计划 #1

候选情况下, 能够防止在回调闭包中捕获任何状况. 举个比如, 仅仅捕获对象的ID, 并回调中央Controller报告具有该ID的项被点击. 交给中央存储经过ID查找最新版本的项并采用正确的举动.

候选计划 #2

假如上面的计划关于你的场景并不适合, 那么能够运用KeyedListener. 它将监听器回谐和值类型进行配对, 任何时分值发生改变, 回调就会被更新. 这是通用的解决计划, 能够进行裁剪以匹配自己的需求.

 class KeyedListener<Key, Listener> private constructor(val identifier: Key, val callback: Listener)  {
    companion object {
        @JvmStatic
        fun <Key, Listener> create(identifier: Key, callback: Listener): KeyedListener<Key, Listener> {
            return KeyedListener(identifier, callback)
        }
    }
    // Only include the key, and not the listener, in equals/hashcode
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is KeyedListener<*, *>) return false
        return identifier == other.identifier
    }
    override fun hashCode() = identifier?.hashCode() ?: 0
}

在Model/View中的用法像这姿态:

@ModelProp(Option.NullOnRecycle)
fun setKeyedOnClickListener(listener: KeyedListener<*, OnClickListener>?) {
   setOnClickListener(keyedListener?.callback);
}

创立Model时的用法像这姿态:

 animals.forEach {
   animalModel {
     id(it.id)
     keyedOnClickListener(KeyedListener.create(it) { v: View -> // do something with the animal }
   }
 }