これまでRecyclerViewでのComposeの使い方はonBindViewHolderでsetContentViewで行うようなスクロールのたびにComposableを作り直すような形でやっていました。
RecyclerViewには以下のようなライフサイクルがありますが、このアイテムのライフサイクルでやるしかなかったです。
RecyclerView自体のインスタンス
Viewのライフサイクル (画面分しか作らないで使い回す)
アイテムのライフサイクル (スクロールで使い回されるたびに死んだり生き返ったり)
それがCustomview PoolingcontainerによってViewのライフサイクルで利用できるようになったようです。
サンプルコードとしては以下で利用できます。
仕組み
RecyclerView -> Customview Poolingcontainerライブラリ
Compose -> Customview Poolingcontainerライブラリ
で単にViewのtagにListenerを入れたりして、いい感じに呼ぶだけです。
RecyclerViewコンストラクタでタグを設定する。
PoolingContainer.setPoolingContainer(this, true);
var View.isPoolingContainer: Boolean
get() = getTag(IsPoolingContainerTag) as? Boolean ?: false
set(value) {
setTag(IsPoolingContainerTag, value)
}
ComposeViewをRecyclerViewの子で作る
class AbstractComposeView {
...
private var disposeViewCompositionStrategy: (() -> Unit)? =
ViewCompositionStrategy.Default.installFor(this)
...
val Default: ViewCompositionStrategy
get() = DisposeOnDetachedFromWindowOrReleasedFromPool
...
DisposeOnDetachedFromWindowOrReleasedFromPool.installForでライフサイクルとの紐付け。
object DisposeOnDetachedFromWindowOrReleasedFromPool : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
if (!view.isWithinPoolingContainer) {
view.disposeComposition()
}
}
}
view.addOnAttachStateChangeListener(listener)
val poolingContainerListener = PoolingContainerListener { view.disposeComposition() }
view.addPoolingContainerListener(poolingContainerListener)
return {
view.removeOnAttachStateChangeListener(listener)
view.removePoolingContainerListener(poolingContainerListener)
}
}
}
addPoolingContainerListener()はViewのタグにリスナーを入れるだけ
fun View.addPoolingContainerListener(listener: PoolingContainerListener) {
this.poolingContainerListenerHolder.addListener(listener)
}
private val View.poolingContainerListenerHolder: PoolingContainerListenerHolder
get() {
var lifecycle =
getTag(PoolingContainerListenerHolderTag) as PoolingContainerListenerHolder?
if (lifecycle == null) {
lifecycle = PoolingContainerListenerHolder()
setTag(PoolingContainerListenerHolderTag, lifecycle)
}
return lifecycle
}
private class PoolingContainerListenerHolder {
private val listeners = ArrayList<PoolingContainerListener>()
fun addListener(listener: PoolingContainerListener) {
listeners.add(listener)
}
fun removeListener(listener: PoolingContainerListener) {
listeners.remove(listener)
}
fun onRelease() {
for (i in listeners.lastIndex downTo 0) {
listeners[i].onRelease()
}
}
}
RecyclerViewでViewが不必要になったタイミングでcallPoolingContainerOnRelease()が呼ばれる
RecyclerView内
PoolingContainer.callPoolingContainerOnRelease(
scrapHeap.get(i).itemView
);
で子孫までViewのタグから取り出して、onRelease()を呼びまくる
fun View.callPoolingContainerOnRelease() {
this.allViews.forEach { child ->
child.poolingContainerListenerHolder.onRelease()
}
}
でこのonReleaseは先程のDisposeOnDetachedFromWindowOrReleasedFromPoolの
// ↓のラムダが呼ばれる
val poolingContainerListener = PoolingContainerListener { view.disposeComposition() }
view.addPoolingContainerListener(poolingContainerListener)
Groupieでどうするか
もともとは こんな感じのコードを書いており、これはbind()でsetContentView{}していました。
class TextComposeItem(val text: String) : ComposeItem() {
@Composable
override fun Content(position: Int) {
Item(text)
}
}
しかし、公式のTestでは以下のような形になっており、ViewのスコープでComposeのMutableStateをもって、変更できるようにしています。
class ComposeItemRow @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var index by mutableStateOf(0)
@Composable
override fun Content() {
key(index) {
ItemRow(index)
}
}
}
bindのタイミングでこのプロパティにあるindexにデータを渡すということを行っていました。
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemRow.index = position
}
このようにComposeに信頼できるStateを見せる必要がありますが、GroupieのItemのフィールドではアイテムとViewに関連がないため、このようなことができません。
今の解決策
なので、今考えられる方法として新しくスコープを作るという方法を考えています。
ただ、この方法はかなりGroupieのシンプルという良いところを無くしてしまうので、他にいい方法を考えたいところです。
class TextComposeItem(val text: String) :
ComposeItem<TextComposeItem.Binding>() {
class Binding : ComposeBinding {
// これはViewのスコープなので、見ても安全
var text by mutableStateOf("")
@Composable
override fun Content() {
Item(text)
}
}
override fun composeBinding(): Binding {
return Binding()
}
override fun bind(composeBinding: Binding, position: Int) {
composeBinding.text = text
}
}
詳しいComposeItemなどの中身はこちら。