4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Modifier.Node を使いましょう (Part 5: 他 Modifier.Node に委任する、DelegatingNode)

Last updated at Posted at 2023-09-10

前の記事では @Composable 関数を Modifier.Node で書き換えるためにいくつかチップスを紹介してます。

シリーズのコンテンツ
Part 1: なぜ Modifier.Node が必要になった
Part 2: Modifier.Node API の紹介
Part 3: Modifier.Node に移行する基本手順
Part 4: @Composable 関数の実装を Modifier.Node に書き換える
Part 5: 他 Modifier.Node に委任する、DelegatingNode(この記事)
Part 6: Modifier.Node 移行しようと思ってまだ残ってる課題

この記事では、Modifier.Node API の1つの側面、Modifier.Node クラスを delegate (委任する・任せる) することについて紹介します。

他 Modifier.Node に委任すること

例えば、Modifier.padding の Modifier.Node に移行された前の実装を確認すると色んなことをやってると確認できます。

fun Modifier.focusable(
    interactionSource: MutableInteractionSource? = null, ..
) = composed {
    // [1]
    Disposable(interactionSource){ /* .. */ }

    // [2]
    val pinnableContainer = LocalPinnableContainer.current
    DisposableEffect(pinnableContainer) { /* .. */ }
    ..

    // [3]
    Modifier
        .semantics { /* .. */ }
        .then(FocusBoundsModifier())
        .bringIntoViewRequester(bringIntoViewRequester)
        ..
        // [4]
        .onFocusChanged { 
            val pinHandle = pinnableContainer?.pin()
            /* .. */

            val interaction = FocusInteraction.Focus()
            interactionSource?.emit(interaction)
            /* .. */
        }
}
  1. 関数パラメータの interactionSource が key になってる DisposableEffect の実装があります。
  2. LocalPinnableContainer の値が key になってる DisposableEffect の実装があります。
  3. Modifier 自体が複数の Modifier に作られてます。
  4. そして、その複数の Modifier の中の 1つの Modifier.onFocusChanged で関数パラメータの interactionSource と LocalPinnableContainer の値を使った実装があります。

前の記事の最後で少し話を降ってみましたが、Modifier.focusable の 全ての実装を 1つの Modifier.Node にまとめて複製してみると以下のように巨大なクラスになる可能性があります。

class Focusable(): Modifier.Node(),
    FocusEventModifierNode, 
    LayoutAwareModifierNode, 
    SemanticsModifierNode,
    GlobalPositionAwareModifierNode,
    CompositinoLocalConsumerModifierNode,
    ObserverModifierNode {

    fun update(interactionSource: MutableInteractionSource?) {
        /* 
            関数パラメータの interactionSource を使った 
            DisposableEffect の実装
        */
    }

    override fun onObservedReadsChanged() {
        observeReads {
            /* 
                LocalPinnableContainer の値を使った 
                DisposableEffect の実装
            */
        }
    }

    override fun onPlaced(coordinates: LayoutCoordinates) {
        /* Modidifer.bringIntoViewRequester の実装 */
    }

    override fun SemanticsPropertyReceiver.applySemantics() {
        /* Modifier.semantics の実装 */
    }
    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
        /* FocusBoundsModifier の実装 */
    }

    override fun onFocusEvent(state: FocusState) {
        /* Modifier.onFocusEvent の実装 */
    }
}

もしそれぞれの実装が互いに依存されてない場合、役割を分担し、それぞれの役割を持つ Modifer.Node に任せれば実装しやすくなるかもしれないです。

この記事の最後で上記の Modifier.padding が他 Modifier.Node に委任してる事例について記載しました。では、委任するため使われてるいくつかの API を紹介します。

DelegatingNode

DelegatingNode に拡張すると、他の Modifier.Node に delegate
して、役割を分担することができます。

abstract class DelegatingNode : Modifier.Node() {
    protected fun <T : DelegatableNode> delegate(node: T): T { /* .. */ }
    protected fun <T : DelegatableNode> undelegate(node: T) { /* .. */ }
}
  • delegate(): このメソッドを使って Modifier.Node が委任されます。パラメータに Modifier.Node の新しいインスタンスを渡すようにします。

onAttach が呼び出された後このメソッドを使って Modifier.Node を委任することはできないです。
つまり、init や onAttach で delegate を使えば大丈夫です。

class SampleNode: DelegatingNode() {
    init {
        // このインスタンスがされます
        val a = delegate(SampleDelegatableNode())
    }

    override fun onAttach() {
        // このインスタンスが委任されます
        val b = delegate(SampleDelegatableNode())
    }

    // ModifierNodeElement.update で呼び出される
    fun update() {
        // このインスタンスが委任されないです
        val c = delegate(SampleDelegatableNode())
    }
}

でも init で delegate を使っても、実際に Modifier.Node が委任されることは DelegatingNode の onAttach の時です。

同じ Modifier.Node のインスタンスを複数の Modifier.Node に委任されることができない。

同じ DelegatingNode で 2つの LayoutModifierNode を委任することはできないです。参考

自動 invalidation が行われる Modifier.Node interface に拡張されてる Modifier.Node に委任したら、DelegatingNode がそのModifier.Node interface に拡張されなくても委任されてる Modifier.Node の 自動 invalidation が行われます。

例えば、

// autoinvalidate される DrawModifierNode に拡張されてない
class SampleNode: DelegatingNode() {
    val delegateNode = delegate(SampleDelegateNode())
}

// autoinvalidate される DrawModifierNode に拡張されてる
class SampleDelegateNode: DrawModifierNode() {
    override fun ContentDrawScope.draw() {
        /* .. */
    }
}

この場合は、Recomposition の時は SamplegNode が DrawModifierNode に拡張されなくても、委任されてる SampleDelegateNode の自動 invalidation 行われて draw が呼び出されます。

  • undelegate() : DelegatingNode が Node チェーンから外された時、委任してる全ての Modifier.Node も外されます。
    もし、委任してる Modifier.Node を手動で外したいならこのメソッドを使います。

DelegatableNode

DelegatableNode に拡張すると、Modifier.Node を DelegatingNode に委任されることができます。しかし、Modifier.Node は DelegatableNode に拡張されてるので、全ての Modifier.Node を既に委任することができます。

interface DelegatableNode { /* .. */ }

interface Modifier {
    abstract class Node: DelegatingNode { /* .. */ }
}

委任可能な Modifier.Node

androidx ライブラリーのほとんどの Modifier が Modifier.Node に移行されてますが、全ての Modifier.Node を public にアクセスできるわけないです。つまり、私たちは全ての Modifier.Node に委任することはできない。

この記事を書いたとき委任されることが可能になってる Modifier.Node を紹介します。

SuspendingPointerInputModifierNode

これは Modifier.pointerInput の Modifier.Node であります。
この Modifier.Node の 新しいインスタンスを返す同名の関数を呼び出して委任することができます。

private class ClickablePointerInputNode(
    var enabled: Boolean,
    var onClick: () -> Unit, ..
): DelegatingNode, PointerInputModifierNode {
    
    private val pointerInputNode = delegating(
        SuspendingPointerInputModifierNode {
            detectTapAndPress(
                onTap = { if (this.enabled) this.onClick() },
                ..
            )
        }
    )

    override fun onPointerEvent(..) {
        pointerInputNode.onPointerEvent(..)
    }
}

PointerInputModifierNode に拡張されてるので、委任されても自動 invalidation が行われず、onPointerEvent が呼び出されない。
つまり、DelegatingNode が PointerInputModifierNode に拡張する必要があり、onPointerEvent コールバックで委任されてるインスタンスの onPointerEvent も呼び出せないとイベントをインターセプトすることができないです。

CacheDrawModifierNode

CacheDrawModifierNodeModifier.drawWithCache に対応してる Modifier.Node です。
この Modifier.Node の 新しいインスタンスを返す同名の関数を呼び出して委任することができます。

class BorderModifierNode(..): DelegatingNode {
    
    private val drawWithCacheModifierNode = delegate(
        CacheDrawModifierNode {
            onDrawWithContent {
                drawContent()
                /* .. */
            }
        }
    )
}

DrawModifierNode に拡張されてるので、委任されても自動 invalidation が行われます。 つまり、DelegatingNode が DrawModifierNode に拡張する必要はないです。

invalidateDrawCache メソッドを呼び出して、手動で invalidate する(再描画する)ことができます。

// ../androidx/compose/foundation/Border.kt
class BorderModifierNode(..): DelegatingNode {
    private val drawWithCacheModifierNode = delegate(
        CacheDrawModifierNode { /* .. */ }
    )

    var brush: Brush
        set(value) {
            drawWithCacheModifierNode.invalidateDrawCache()
            ..
        }
}

Modifier.padding で委任される Modifier.Node に分解

Modifier.padding が巨大クラスになってしまう恐れについて記載しましたが、移行の change list で実際に DelegatingNode を使ってどうやって他 Modifier.Node に役割を委任してるのか紹介しましす。

PinnedContainer 対応

Modifier.focusable では PinnableContainer の実装は2つあります。

fun Modifier.focusable() = composed {
    /* .. */

    // [1]
    val pinnableContainer = LocalPinnableContainer.current
    var pinnedHandle: PinnedHandle by remember { mutableStateOf(null) }
    DisposableEffect(pinnableContainer) { 
        /* .. */
        pinnableContainer?.pin()
        onDispose {
            pinnedHandle?.release()
        }
    }

    // [2]
    Modifier
        .onFocusChanged { focusState ->
            val isFocused = focusState.isFocused
            if (isFocused) {
                /* .. */
                pinnableContainer?.pin()
            } else {
                pinnedHandle?.release()
            }
        } 
}
  1. DisposableEffect の key になってる対応
  2. Modifier.onFocusChanged で isFocused の状態が切り替える対応

PinnableContainer のこの2つの実装が他の実装と依存しないため、PinnedHandle の内部 state を保持する FocusablePinnableContainerNode に実装を委任するように実装されてます。

private class FocusableNode(
    interactionSource: MutableInteractionSource?
) : DelegatingNode(),
    FocusEventModifierNode, 
    .. {
    // [1]
    private val focusablePinnableContainer = delegate(FocusablePinnableContainerNode())
    
    // [2]
    override fun onFocusEvent(focusState: FocusState) {
        val isFocused = focusState.isFocused
        ..
        focusablePinnableContainer.setFocus(isFocused)
    }
}

private class FocusablePinnableContainerNode : Modifier.Node(),
    CompositionLocalConsumerModifierNode, 
    ObserverModifierNode, .. {

    private var pinnedHandle: PinnableContainer.PinnedHandle? = null

    fun setFocus(isFocused: Boolean) {
        if (isFocused) {
            val pinnableContainer = retrievePinnableContainer()
            this.pinnedHandle = pinnableContainer?.pin()
            ..
        } else {
            this.pinnedHandle?.release()
        }
    }

    // [3]
    override fun onObservedReadsChanged() {
        observeReads {
            val pinnableContainer = retrievePinnableContainer()
            /* .. */
            this.pinnedHandle?.release()
            this.pinnedHandle = pinnableContainer?.pin()
        }
    }

    // [4]
    private fun retrievePinnableContainer(): PinnableContainer? {
        var container: PinnableContainer? = null
        observeReads {
            container = currentValueOf(LocalPinnableContainer)
        }
        return container
    }
}
  1. delegate を使って pinnedHandle を保持する FocusablePinnableContainerNode の新しいインスタンス渡されるようになってます。
  2. そして、isFocused の切り替えのため、FocusableNode が拡張された FocusEventModifierNode の onFocusEvent コールバックから FocusablePinnableContainerNode に isFocused の状態が渡されます。
  3. CompositionLocalなため、DisposableEffect の実装が onObservedReadsChanged で行われます。
  4. そして、継続的に CompositionLocal を観察のため observeReads を使って CompositionLocal の値を読み込みされてます。

interactionSource パラメータ対応

Modifier.focusable では関数パラメータの interactionSource の実装は2つあります。

fun Modifier.focusable(
    interactionSource: MutableInteractionSource? = null, ..
) = composed {
    /* .. */
    // [1]
    DisposableEffect(interactionSource) {
        onDispose {
            interactionSource.emit(FocusInteraction.Unfocus(..))
        }
    }

    // [2]
    Modifier
        .onFocusChanged { focusState ->
            val isFocused = focusState.isFocused
            if (isFocused) {               
                interactionSource.emit(FocusInteraction.Unfocus(..))
                interactionSource.emit(FocusInteraction.Focus())
                ..
            } else {
                interactionSource.emit(FocusInteraction.Unfocus(..))
            }
        }
}
  1. Disposable の key になってる対応
  2. Modifier.onFocusChanged で isFocused の状態が切り替える対応

PinnableContainer のこの2つの実装が他の実装と依存しないため、FocusableInteractionNode に実装を委任するするように実装されてます。

private class FocusableNode(
    interactionSource: MutableInteractionSource?
) : DelegatingNode(),
    FocusEventModifierNode, 
    .. {
    // [1]
    private val focusableInteractionNode = delegate(FocusableInteractionNode(interactionSource))

    // [2]
    override fun onFocusEvent(focusState: FocusState) {
        ..
        focusableInteractionNode.setFocus(isFocused)
    }
    
    // [3]
    // ModifierNodeElement.update() メソッドで呼び出される
    fun update(interactionSource: MutableInteractionSource?) =
        focusableInteractionNode.update(interactionSource)
}

private class FocusableInteractionNode(
    private var interactionSource: MutableInteractionSource?
) : Modifier.Node() {
    
    fun setFocus(isFocused: Boolean) {
        interactionSource?.let { interactionSource ->
            if (isFocused) {               
                interactionSource.emit(FocusInteraction.Unfocus(..))
                interactionSource.emit(FocusInteraction.Focus())
                ..
            } else {
                interactionSource.emit(FocusInteraction.Unfocus(..))
            }
        }
    }

    // [4]
    fun update(interactionSource: MutableInteractionSource?) {
        if (this.interactionSource != interactionSource) {
            interactionSource.emit(FocusInteraction.Unfocus(..))
            this.interactionSource = interactionSource
        }
    }
}
  1. interactionSource を持ってる新しい FocusableInteractionNode の Modifier.Node が作成され、委任されます。

  2. Modifier.composed バージョンでは、focus 状態が変更されたときにinteractionSource の処理が行われました。委任されてる FocusableInteractionNode に isFocused の値が渡され、同じ処理が行われます。

  3. DisposableEffect の key になった対応のため、DelegatingNode の FocusableNode の update で渡された interactionSource を FocusableInteractionNode に渡せるような実装になってます。

  4. そして、FocusableInteractionNode で DisposableEffect 実装が行われてます。


こんなふうに、DelegatingNode を使って 他の Modifier.Node に役割を分担してModifier.Node の移行の実装をしやすく設計に行われることができます。次の最後の記事では、これから Modifier.Node に移行しようと思っていても、いくつか残ってる課題を紹介しようと思ってます。興味あればぜひ読んでみてください。

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?