LoginSignup
3
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-06-07

これまで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などの中身はこちら。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2