RecyclerViewとComposeのライフサイクルを統合する新しいCustomview Poolingcontainer Libraryの仕組みとGroupieでどうするか

これまでRecyclerViewでのComposeの使い方はonBindViewHolderでsetContentViewで行うようなスクロールのたびにComposableを作り直すような形でやっていました。

RecyclerViewには以下のようなライフサイクルがありますが、このアイテムのライフサイクルでやるしかなかったです。

RecyclerView自体のインスタンス
Viewのライフサイクル (画面分しか作らないで使い回す)
アイテムのライフサイクル (スクロールで使い回されるたびに死んだり生き返ったり)

それがCustomview PoolingcontainerによってViewのライフサイクルで利用できるようになったようです。

サンプルコードとしては以下で利用できます。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/recyclerview/RecyclerViewInteropDemoFragment.kt;drc=212a307f7988ba9be77a1e56558fcf4f3f5d6f50



# 仕組み

RecyclerView -> Customview Poolingcontainerライブラリ
Compose -> Customview Poolingcontainerライブラリ

で単にViewのtagにListenerを入れたりして、いい感じに呼ぶだけです。

## RecyclerViewコンストラクタでタグを設定する。

```java
   PoolingContainer.setPoolingContainer(this, true);
```


```kotlin
var View.isPoolingContainer: Boolean
    get() = getTag(IsPoolingContainerTag) as? Boolean ?: false
    set(value) {
        setTag(IsPoolingContainerTag, value)
    }
```

# ComposeViewをRecyclerViewの子で作る

```kotlin
class AbstractComposeView {
...
    private var disposeViewCompositionStrategy: (() -> Unit)? =
        ViewCompositionStrategy.Default.installFor(this)

...
        val Default: ViewCompositionStrategy
            get() = DisposeOnDetachedFromWindowOrReleasedFromPool
...
```

# DisposeOnDetachedFromWindowOrReleasedFromPool.installForでライフサイクルとの紐付け。

```kotlin
    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のタグにリスナーを入れるだけ


```kotlin
fun View.addPoolingContainerListener(listener: PoolingContainerListener) {
    this.poolingContainerListenerHolder.addListener(listener)
}
```

```kotlin
private val View.poolingContainerListenerHolder: PoolingContainerListenerHolder
    get() {
        var lifecycle =
            getTag(PoolingContainerListenerHolderTag) as PoolingContainerListenerHolder?
        if (lifecycle == null) {
            lifecycle = PoolingContainerListenerHolder()
            setTag(PoolingContainerListenerHolderTag, lifecycle)
        }
        return lifecycle
    }
```


```kotlin
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内

```java
    PoolingContainer.callPoolingContainerOnRelease(
      scrapHeap.get(i).itemView
    );
```

で子孫までViewのタグから取り出して、onRelease()を呼びまくる

```
fun View.callPoolingContainerOnRelease() {
    this.allViews.forEach { child ->
        child.poolingContainerListenerHolder.onRelease()
    }
}
```

でこのonReleaseは先程のDisposeOnDetachedFromWindowOrReleasedFromPoolの

```kotlin
　　　　　　　　　　　　　　　　　　　　　　　　　// ↓のラムダが呼ばれる
            val poolingContainerListener = PoolingContainerListener { view.disposeComposition() }
            view.addPoolingContainerListener(poolingContainerListener)
```


# Groupieでどうするか

もともとは　こんな感じのコードを書いており、これはbind()でsetContentView{}していました。

https://github.com/takahirom/groupie-compose-item/blob/f074e32b54b93455e98e84e0553868ef3ba36e85/composeitem/src/main/java/com/github/takahirom/composeitem/ComposeItem.kt

```kotlin
class TextComposeItem(val text: String) : ComposeItem() {
  @Composable
  override fun Content(position: Int) {
    Item(text)
  }
}
```

しかし、公式のTestでは以下のような形になっており、ViewのスコープでComposeのMutableStateをもって、変更できるようにしています。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/recyclerview/RecyclerViewInteropDemoFragment.kt;l=110?q=PoolingContainer%20compose

```kotlin
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にデータを渡すということを行っていました。

```kotlin
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            holder.itemRow.index = position
        }
```

このようにComposeに信頼できるStateを見せる必要がありますが、**GroupieのItemのフィールドではアイテムとViewに関連がないため、このようなことができません。**

## 今の解決策

なので、今考えられる方法として新しくスコープを作るという方法を考えています。

ただ、この方法はかなりGroupieのシンプルという良いところを無くしてしまうので、他にいい方法を考えたいところです。

```kotlin
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などの中身はこちら。

https://github.com/takahirom/groupie-compose-item/commit/5728c5a1b04d289554b5a98e9c20d59fb185319b#diff-584fb6e98904c06dcac22c220edf9cd5413f1cb6bb9e4ceb25a1b27ce44b2a69R9