♻️

RecycledViewPoolをいい感じにFragment間で共有する

2023/08/15に公開

本稿は2019年に執筆したものをZennに移行したものです。

RecycledViewPool

RecyclerViewはスクロールして描画されなくなったchild viewなどをリサイクルしてくれます。リサイクルされたviewはRecyclerView内部のRecyclerが保持しているRecycledViewPool内にキャッシュされます。
このRecycledViewPoolですが、RecyclerView#setRecycledViewPoolを利用することでRecyclerView間で共有することが可能になります。
LinearLayoutManager#setRecycleChildrenOnDetachをつかってRecyclerViewがwindowからdetachされた際にchild viewsがリサイクルされるようにした上で、Fragment間でRecycledViewPoolを共有すればFragmentを切り替えた際のviewのinflate回数を減らせます。ViewPagerなんかで効果的でしょう。

EpoxyRecyclerView

Epoxyはかなり多機能で、提供されているクラスの中にEpoxyRecyclerViewなるものがあります。
こいつはRecyclerViewを継承し、EpoxyとRecyclerViewの連携を強めるメソッドの実装や、彼らなりの改良を加えてます。

EpoxyRecyclerViewによるRecycledViewPoolの共有

EpoxyRecyclerViewは、Activity内のEpoxyRecyclerView間で同一のRecycledViewPoolを共有するように実装されています。
共有されるRecycledViewPoolはUnboundedViewPoolというEpoxy同梱のRecycledViewPoolを継承したクラスです。RecyclerView.RecycledViewPoolはViewTypeごとに5個のViewをキャッシュするようになっていますが、UnboundedViewPoolはその上限を取っ払っています。
このActivity内で共有されたUnboundedViewPoolにキャッシュされたViewはActivityのonDestroyでclearされます(ArchitectureComponentsのLifecycleを利用している場合。Lifecycleを利用していない場合はActivityが破棄されていることが確認できたら随時)。

機能ごとにActivityを分けているのであればこの機能は良いかもしれませんが、今開発しているアプリは1Activityの構成になっているのでキャッシュの生存期間が非常に長くなってしまうのでこの機能は使わないことにしました。

RecycledViewPoolをいい感じにFragment間で共有する

1Activity構成のアプリではActivityのスコープではなくFragmentのスコープでRecycledViewPoolを共有できたほうが嬉しいので、EpoxyをパクってFragment間でRecycledViewPoolを共有するためのコードを試しに書いてみました。ArchitectureComponentsのLifecycleを利用している前提のコードです。

class RecycledViewPoolFactory {

  private val viewPools = SparseArray<RecycledViewPool>()

  fun create(activity: ComponentActivity) = createByLifecycleOwner(activity)

  fun create(fragment: Fragment) = createByLifecycleOwner(fragment)

  private fun createByLifecycleOwner(lifecycleOwner: LifecycleOwner): RecycledViewPool {
    viewPools.get(lifecycleOwner.hashCode())?.let { return it }

    val viewPool = RecycledViewPool()
    viewPools.put(lifecycleOwner.hashCode(), viewPool)

    lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
      if (event == Lifecycle.Event.ON_DESTROY) {
        viewPool.clear()
        viewPools.remove(lifecycleOwner.hashCode())
      }
    })

    return viewPool
  }
}

gist

使い方はこんな感じです。

// 親Activity or 親Fragmentを使うパターン
parentFragment?.let {
    recyclerView.setRecycledViewPool(recycledViewPoolFactory.create(it))
}
activity?.let {
    recyclerView.setRecycledViewPool(recycledViewPoolFactory.create(it))
}

// 自身(Activity or Fragment)をつかうパターン
recyclerView.setRecycledViewPool(recycledViewPoolFactory.create(this))

RecycledViewPoolFactoryによって生成されたRecycledViewPoolは生成するのに利用したActivity/FragmentのonDestroyでclearされた上でFactory内から削除されます。RecycledViewPoolFactoryはDaggerでSingletonにしてActivityやFragmentにinjectしたりするといいでしょう。

おまけ

EpoxyRecyclerViewに実装されている機能(EpoxyとRecyclerViewの連携のための実装除く)

  • デフォルトでLinearLayoutManagerが利用されるようになっている
  • EpoxyControllerと連携してGridLayoutManagerのspanCountを更新する
  • attributeでitem間に任意のスペースを設定できる(Epoxy同梱のEpoxyItemSpacingDecoratorを使ってoffsetをつけてくれる)
  • デフォルトでEpoxy同梱のUnboundedViewPoolを利用する
    (RecycleView.RecycxledViewPoolはデフォルトではViewTypeごとに5個しかViewをpoolしないが後者は上限なし)
  • Activity内のEpoxyRecyclerView間で同一のRecycledViewPoolを共有する

Discussion