Jetpack Compose Interop: Using Compose in a RecyclerView
一言で表すと
RecylerViewでComposeViewを使うときの注意点と以前の対処方法について
概要
Compose UI: 1.2.0-beta02以前では
RecyclerViewのアイテムにComposableを使用するとViewが画面から離れるとコンポジションが破棄される
画面から外れたり移動する際に断続的にコンポジションの破棄と再作成を繰り返す
コストがかかりパフォーマンスに影響を与えていた
Compose UI 1.2.0-beta02 および RecyclerView 1.3.0-alpha02 から
ライブラリで使用されるView Composition Strategyが以下のように変更された
RecyclerViewなどのプーリングコンテナの一部でない限りは画面から離れたときに自動的にコンポジションを破棄する
RecyclerViewのアイテムとして使用されるとコンポーザブルが破棄されずに再利用される
code:kotlin
class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): MyComposeViewHolder {
return MyComposeViewHolder(ComposeView(parent.context))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind("$position")
}
}
class MyComposeViewHolder(
val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
fun bind(input: String) {
composeView.setContent {
MdcTheme {
Text(input)
}
}
}
}
以前のデフォルトのViewCompositionStrategyは DisposeOnDetachedFromWindow だった
https://miro.medium.com/max/1260/1*RtZFZdJVa5-Sxw-MSqSpfA.png
ビューがウィンドウから離れるとコンポジションが破棄される
リストを素早く移動するとスクロールのパフォーマンスが低下する
以前ではonViewRecycled(ViewHolder)をオーバーライドして再利用されたときに破棄することでコンポジションの破棄を改善していた
code:kotlin
class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): MyComposeViewHolder {
return MyComposeViewHolder(ComposeView(parent.context))
}
override fun onViewRecycled(holder: MyComposeViewHolder) {
holder.composeView.disposeComposition()
}
}
さらにComposeViewがViewが切り離されたときに が破棄されないようにするにViewCompositionStrategyをDisposeOnViewTreeLifecycleDestroyedに設定
ライフサイクルの所有者が破棄されたときにコンポジションが破棄されるように設定
新しいCompositionStrategyとしてDisposeOnDetachedFromWindowOrReleasedFromPoolがデフォルトになった
プーリングコンテナ内でない場合は以前(DisposeOnDetachedFromWindow)と同じ動作をする
プーリングコンテナはアイテムが保持しているリソースを破棄すべき時に通信できるようにする
PoolingContainerListenerのコールバックでView階層外にある子のライフサイクルを管理するコンテナ(RecyclerViewなど)内で子Viewに 子が保持しているリソースを廃棄する時 を知らせる
コンポジションを最適にアタッチする必要がある時にComposeと通信できるようになった
RecyclerViewではPoolingContainerによってアイテムビューが破棄された時にコンポジションも破棄されるようになった
RecycledViewPoolがすでに満杯の場合やRecyclerViewがウィンドウからデタッチされている場合など
アイテムが再利用されるケースではコンポジションが保持されているため、内部的に記憶された状態も記憶される
以下のようにRecyclerView内にLazyRowがあるようなケースでは再利用されたViewではスクロール位置が記憶される問題がある
https://miro.medium.com/max/700/1*Q5TVebKDm9d1NEkNsEDxbA.gif
これを回避する方法は2つある
アイテム固有の状態をアダプタに引き上げる
code:kotlin
@Composable
fun ItemRow(index: Int, state: LazyListState) {
Column(Modifier.fillMaxWidth()) {
Text("Row #${index + 1}", Modifier.padding(horizontal = 8.dp)) LazyRow(state = state) {
// ...
}
}
}
class RecyclerViewAdapter : RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
private val rowStates = mutableMapOf<Int, LazyListState>()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemRow: ComposeItemRow = itemView.findViewById(R.id.itemRow)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(inflater.inflate(R.layout.interop_demo_row, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val state = rowStates.getOrPut(position) { LazyListState() }
holder.itemRow.index = position
holder.itemRow.rowState = state
}
override fun getItemCount(): Int = 50
}
class ComposeItemRow @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var index by mutableStateOf(0)
var rowState: LazyListState? by mutableStateOf(null)
@Composable
override fun Content() {
ItemRow(index, rowState!!)
}
}
アイテムを一意に識別する1つまたは複数の値を渡して状態を持つものをすべてラップする
コンポジションの関連部分を再度作成するためパフォーマンスが低下する危険性があるので状態を持つ UI の部分に対してのみ行う
code:kotlin
@Composable
fun ItemRow(index: Int) {
Column(Modifier.fillMaxWidth()) {
Text("Row #${index + 1}", Modifier.padding(horizontal = 8.dp)) key(index) {
LazyRow {
// ...
}
}
}
}
デタッチされたアイテムでもアニメーションなどの状態変化に応じた再構成が引き続き行われるようになり、パフォーマンスが悪化する危険がある
アイテムが画面から消える時にアニメーションを停止するようにする
code:kotlin
class ComposeItemRow @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
// …
// State hoist animatable object so that it can be explicitly stopped
val animatable = Animatable(Color.Gray)
@Composable
override fun Content() {
key(index) {
ItemRow(index, animatable)
}
}
}
class MainAdapter : RecyclerView.Adapter<MainAdapter.ViewHolder>() {
// …
override fun onViewDetachedFromWindow(holder: ViewHolder) {
super.onViewDetachedFromWindow(holder)
// Stop the animation when the view gets detached
viewLifecycleOwner.lifecycleScope.launch {
holder.itemRow.animatable.stop()
}
}
}
気になるポイント
chigichan24.icon recycler viewで compose を使うのをやめよう(?)
メモ
コメント