Modifier.Node を使いましょう (Part 3: Modifier.Node に移行する基本手順)

[この前の記事](https://qiita.com/_SUR4J_/items/8047da7f90ddb9433539)では、新しい Modifier.Node 仕組みそして少しだけこの仕組みの内部的な実装を紹介してみました。

|シリーズのコンテンツ|
|--|
|Part 1: [なぜ Modifier.Node が必要になった](https://qiita.com/_SUR4J_/items/6d18caffd4897a39d010)|
|Part 2: [Modifier.Node APIの紹介](https://qiita.com/_SUR4J_/items/8047da7f90ddb9433539)|
|Part 3: Modifier.Node に移行する基本手順（この記事） |
|Part 4: [`@Composable` 関数の実装を Modifier.Node に書き換える](https://qiita.com/_SUR4J_/items/d48372b5793c4a0fa65f)|
|Part 5: [Modifier.Node を委任する、Delegate パターン](https://qiita.com/_SUR4J_/items/134fcd5a3bc26d01dd78)|
|Part 6: [Modifier.Node 移行しようと思ってまだ残ってる課題](https://qiita.com/_SUR4J_/items/fda5fca9170792c0cf1b) |

この記事の内容を少し理解しやすくためにまずは、いくつかの Modifier.Node の Interface を紹介します。

# Modifier.Node interface の紹介
Modifier.Node のクラスを１つや複数の Modifier.Node interface に拡張して UIコンポーネントに特定なプロパティを提供することができます。
[NodeKind.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt?q=NodeKind) を確認したら全ての interface を確認できますが、その中でいくつかを紹介します。

## LayoutModifierNode
[LayoutModifierNode](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt) UIコンポーネントのサイズや位置を調整できるための Modifier.Node です。
これを拡張すると、`measure` メソッドを　override してUIコンポーネントの MeasureResult（サイズ調整と配置）を計算します。
```kotlin
interface LayoutModifierNode : DelegatableNode {
    fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult
}
```

:::note info
ModifierNodeElement.update が呼び出された後 `measure` メソッドが自動で呼び出されます。
:::

もし、`shouldAutoInvalidate` を false に設定したら、[`invalidatePlacement()`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt?q=invalidatePlacement) と [`invalidateMeasurement()`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt?q=invalidateMeasurement) を使って手動で次のフレームで位置やサイズを手動で再調整することもできます。

## DrawModifierNode
UIコンポーネントの描画を調整するために[DrawModifierNode](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt) に拡張します。
`draw` メソッドを override して描画を調整することができます。
```kotlin
interface DrawModifierNode : Modifier.Node {
    fun ContentDrawScope.draw()
}
```
:::note info
ModifierNodeElement.update が呼び出された後、`draw` メソッドが自動で呼び出されます。
:::
もし、`shouldAutoInvalidate` を false に設定したら、手動で再描画するために [`invalidateDraw()`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt%3Bl=39?q=DrawModifierNode.invalidateDraw) を使えます。

## PointerInputModifierNode
タップ、ダブルタップ、長押しなどユーザーのタッチインプットをインターセプトするために 
 [PointerInputModifierNode](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputModifierNode.kt) に拡張します。
タッチイベントがインターセプトされたとき `onPointerEvent` が呼び出されます。

```kotlin
interface PointerInputModifierNode : DelegatableNode {
    fun onPointerEvent(
        pointerEvent: PointerEvent,
        pass: PointerEventPass,
        bounds: IntSize
    )
}
```
:::note warn
onPointerEvent が各 Recomposition で呼び出されないです。
:::

## FocusEventModifierNode
UIコンポーネントのフォーカス状態の変更を観察するために [`FocusEventModifierNode`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt%3Bl=33?q=FocusEventModifierNode&sq=) を拡張します。
```kotlin
interface FocusEventModifierNode : Modifier.Node {
    fun onFocusEvent(focusState: FocusState)
}
```
:::note info
ModifierNodeElement.update が呼び出された後、または UIコンポーネントのフォーカス状態が変更されたら onFocusEvent が呼び出されます。
:::

## 自動 invalidation 行われる Modifier.Node interface
ModifierNodeElement.update が呼び出された後や Modifier.Node がはじめて Node チェーンに追加された後、上記の DrawModifierNode のようにいくつかの Modifier.Node interface の invalidation メソッドが自動で呼び出されます。
`shouldAutoInvalidate` が false に設定しなかったら（デフォルトは true）、手動でそれぞれの invalidation メソッドを呼び出さなくても大丈夫です。

どの Modifier.Node interface の invalidation が自動で行われるのか [autoInvalidateNodeSelf](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt%3Bl=252?q=autoInvalidateNodeSelf&sq=&ss=androidx/platform/frameworks/support) の実装を確認すればわかります。

```kotlin
private fun autoInvalidateNodeSelf(node: Modifier.Node, ..) {
    if (node is LayoutModifierNode) {
        node.invalidateMeasurement()
    }
    if (node is DrawModifierNode) {
        node.invalidateDraw()
    }

    /* .. */
    if (node is FocusEventModifierNode) {
        node.invalidateFocusEvent()
    }
}
```

# Modifier.Node に移行するための基本手順
これからは既存の Modifier を Modifier.Node API に書き換えるため基本的な手順を実際に移行された AOSP の 2つの Modifier の例を参考にして紹介します。

:::note warn
**Modifier.Node に移行された Modifier は Modifier.composed で作られたものに限られてない。**
全ての Modifier の実装を統一するために、Modifier.then() に statelessな Modifeir.Element を渡すような Modifier も、既に Modifier.Node に書き換えられてます。
:::

## Modifier.focusable
以下は Modifier.Node に移動された前の Modifier.focusable の実装のざっくりなバージョンです。

```kotlin
fun Modifier.focusable(
    interactionSource: MutableInteractionSource?,
) = composed {
    // [1]
    var isFocused by remember { mutableStateOf(false) }
    val focusRequester = remember { FocusRequester() }

    // [2]
    DisposableEffect(interactionSource) { /* .. */ }
    
    /* .. */

    // [3]
    Modifier
        .semantics { /* .. */ }
        .then(FocusedBoundsModifier())
        ..
        .onFocusChanged {
            isFocused = it.isFocused
            if (isFocused) { /* .. */ } 
            else { /* .. */ }
        }
}
```
1. Modifier.focusable の中に保持されてる state です。
2. 関数パラメータの interactionSource がキーになってる DisposableEffect の副反応があります。
3. 最後に Modifier.focusable は複数の Modifier の組み合わせで作られてるように見えてます。

Modifier.focusable の移行の change listはこちらです:

https://android-review.googlesource.com/c/platform/frameworks/support/+/2450683

これを参照して、以下の手順を考えられます。

### Modifier.Node と ModifierNodeElement クラスの作成
```kotlin
// [1]
fun Modifier.focusable(interactionSource: MutableInteractionSource?)
    = this.then(FocusableElement(interactionSource))

// [2]
private class FocusableElement(
    private val interactionSource: MutableInteractionSource?
) : ModifierNodeElement<FocusableNode>() { /* .. */ }

// [3]
private class FocusableNode(
    var interactionSource: MutableInteractionSource?
): Modifier.Node() { /* .. */ }
```
1. Modifier.focusable 関数を composed から Modifier.then に切り替え、作成する ModifierNodeElement クラスのインスタンスを渡すようにします。
2. ModifierNodeElement クラスの命名は `{Modifier名}Element` のようになります。
そして、Modifier 関数の関数パラメータを ModifierNodeElement クラスのコンストラクタパラメータとして設定します。
3. Modifeir.Node クラスの命名は `{Modifier名}Node` のようになります。
大体 Modifier の関数パラメータが変わると Modifier が変更されたという判断になるので、関数パラメータを Modifier.Node の内部の「変更が可能」な state (`val` より `var`)として保持するようにします。

### ModifierNodeElement クラスの実装
```kotlin
private class FocusableElement(
    private val interactionSource: MutableInteractionSource?
) : ModifierNodeElement<FocusableNode>() {
    // [1]
    override fun create(): FocusableNode =
        FocusableNode(interactionSource)

    // [2]
    override fun update(node: FocusableNode) {
        node.update(interactionSource)
    }

    // [3]
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is FocusableElement) return false

        if (interactionSource != other.interactionSource) return false
        return true
    }
    override fun hashCode(): Int {
        return interactionSource.hashCode()
    }
}

```
1. `create` を override し Modifier.Node の新しいインスタンスを返すようにします。
2.  `update` を override し Modifier.Node のプロパティなどを更新と invalidation の実装を行います。
もし Modifier.Node クラス が Recomposition特に自動で invalidation が行われる Modifier.Node interface を拡張したら、プロパティを更新することも大丈夫かもしれないです。例えば、Modifier.background の BackgroundNode が DrawModifierNode を拡張してるので、`draw` メソッドが Recomposition時に自動で呼び出されます。
    ```kotlin
    class BackgroundNode(var color: Color, var shape: Shape, ..)
        : DrawModifierNode, Modifier.Node() {

        // 各 Recomposition で自動で呼び出されます
        override fun ContentDrawScope.draw() { 
            if (this.shape is RoundedRectangle)
                drawRoundRect(color = this.color, ..)
            drawContent() 
        }
    }
    ```
    この場合、BackgroundElement の update の実装は以下のようになってます。
    ```kotlin
    class BackgroundElement(
        val color: Color, val shape: Shape, ..
    ): ModifierNodeElement<BackgroundNode>() {
        
        override fun update(node: BackgroundNode) { 
            node.color = color
            node.shape = shape
            // node.invalidateDraw() を呼び出さなくても大丈夫
        }
    }
    ```
3. `equals` と `hashCode` を override して、「Modifier が変更された」という判断のための実装を行います。大体インスタンスのタイプとコンストラクタパラメータを比較するような実装を行います。

### Modifier.Node クラスの実装
先も見ましたが、Modifier.focusable が複数の Modifier の組み合わせで作られてます。すべての Modifier の実装を複製しないといけないです。
その中で1つの Modifier、`Modifier.onFocusEvent { .. }` を複製することを例として紹介します。

```kotlin
class FocusableNode(
    var interactionSource: MutableInteractionSource?
): Modifier.Node(), 
    // [1]
    FocusEventModifierNode, .. {
    // [2]
    private var focusedInteraction: FocusInteraction.Focus? = null
    
    // [3]
    override fun onFocusEvent(focusState: FocusState) { 
        // [4]
        if (isAttached) {
            coroutineScope.launch {
                // [5]
                if (this.isFocused) {
                    val interaction = FocusInteraction.Focus()
                    this.interactionSource.emit(interaction)
                    this.focusedInteraction = interaction
                } else {
                    val interaction = FocusInteraction.UnFocus(this.focusedInteraction)
                    this.interactionSource.emit(interaction)
                    this.focusedInteraction = null
                }
            }
        }
    }
}
```


1. Modifier.onFocusChanged の内部的な実装を確認すると、Modifier.Node クラスが[`FocusEventModifierNode`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt) に拡張されてます。なので、FocusableNode も同様で FocusEventModifierNode に拡張します。
    ```kotlin
    private class FocusEventNode(..) 
        : FocusEventModifierNode, Modifier.Node() { /* .. */ }
    ```
2. Modifier.focusable が持ってる内部 state を保持します。Modifier.focusable では実際に remember を使って state が保持されましたが、
    ```kotlin
    fun Modifier.focusable(..) = composed {
        var focusedInteraction by remember { mutableStateOf(false) }
    }
    ```
    Modifier.Node の中で remember のような `@Composable` 関数を呼び出せないので、それをどうやって書き換えるのか次の記事で紹介してます。

3. Modifier.onFocusChanged の FocusEventNode が override された `onFocusEvent` メソッドで、関数パラメータで渡されたラムダーをそのまま呼び出されてます。
    ```kotlin
    private class FocusEventNode(
        var onFocusEvent: (FocusState) -> Unit
    ): FocusEventModifierNode, Modifier.Node() { 

        override fun onFocusEvent(focusState: FocusState) {
            this.onFocusEvent.invoke(focusState)
        }
    }
    ```
    つまり、FocusableNode でこの実装を複製すると、Modifier.onFocusEvent のラムダーを `onFocusEvent` の中で実装を行います。
4. Modifier.onFocusEvent で coroutine が実行されるような実装になってました。
    ```kotlin
    fun Modifier.focusable() = composed {
        /* .. */
        Modifier
            .onFocusEvent {
                scope.launch { /* .. */ }
            }
    }
    ```
    同様で Modifier.Node の coroutineScope を使って coroutine を実行します。
[前の記事](https://qiita.com/_SUR4J_/private/8047da7f90ddb9433539)でも記載しましたが coroutineScope を使って onAttach と onDetach の間でしか coroutine を実行させることができない。
onFocusEvent コールバックが呼び出されるタイミングが明確ではないため、isAttached のチェックを入れてから coroutine を実行させてます。
5. Modifier.onFocusEvent のラムダー内の実装を Modifier.Node の `onFocusEvent` コールバックの中で複製します。Modifier.focusable の中の実装以下のようでした。
    ```kotlin
    fun Modifier.focusable() = composed {
        Modifier
            .onFocusEvent {
                scope.launch {
                    /* .. */
                    if (isFocused) {
                        val interaction = FocusInteraction.Focus()
                        interactionSource.emit(interaction)
                        focusedInteraction.value = interaction
                    } else {
                        val interaction = FocusInteraction.UnFocus(this.focusedInteraction)
                        interactionSource.emit(interaction)
                        focusedInteraction.value = null
                    }
                }
            }
    }
    ```

## Modifier.padding
Modifier.composed で作られてない Modifier の1つ、Modifier.padding の例も紹介します。

Modifier.padding の移行の change listはこちらです:

https://android-review.googlesource.com/c/platform/frameworks/support/+/2435455

移動前の Modifier.padding は以下のようになってました。
```kotlin
// [1]
fun Modifier.background(color: Color, shape: Shape) 
    = this.then(Background(color = color, shape = shape))

// [2]
private class Background(
    val color: Color, 
    val shape: Shape
) : DrawModifier {

    // [3]
    override fun DrawScope.draw() {
        if (this.shape === RectangleShape) {
            drawRect(color = this.color)
        } else { /* .. */ }

        drawContent()
    }

    // [4]
    override fun equals(other: Any?): Boolean {
        val otherModifier = other as? PaddingModifier ?: return false
        return this.color == otherModifier.color &&
            this.shape == otherModifier.shape
    }
}
```
1. Modifier.then に stateless な Modifier.Element の Background の新しいインスタンスが渡されてます。
2. Modifier の関数パラメータがそのまま Background のコンストラクタパラメータとして渡されてます。そして、コンテンツの描画を背後に背景を描画するので、[`DrawModifier`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt%3Bl=43?q=DrawModifier&ss=androidx/platform/frameworks/support) に拡張されてます。
3. DrawModifier の `draw` メソッドを override して描画の実装が行われました。
コンテンツの背後に描画が行われますので、`drawContent()` を最後に呼ばれてます。
4. 前の記事でも話しましたが、Modifier で変更されたかどうかの判断のため Modifier.Element が比較されました。
Modifier.background の関数パラメータが変わるとコンテンツの背景のプロパティも変わって再描画する必要になりますので、`equals` ロジックで渡されてる 2つのパラメータが比較されてます。

移行した後、以下のようになってます。
```kotlin
// [1]
fun Modifier.background(color: Color, shape: Shape) 
    = this.then(Background(color = color, shape = shape))

// [2]
private class BackgroundElement(
    val color: Color, 
    val shape: Shape
) : ModifierNodeElement<BackgroudNode>() {

    // [3]
    override fun create(): BackgroundNode {
        return BackgroundNode(color = color, shape = shape)
    }

    // [4]
    override fun update(node: BackgroundNode) {
        node.color = this.color
        node.shape = this.shape
    }

    // [5]
    override fun equals(other: Any?): Boolean {
        val otherModifier = other as? PaddingModifier ?: return false
        return this.color == otherModifier.color &&
            this.shape == otherModifier.shape
    }
}

// [6]
private class BackgroundNode(
    // [7]
    var color: Color, 
    var shape: Shape
) : DrawModifierNode, Modifier.Node() {

    // [8]
    override fun DrawScope.draw() {
        if (this.shape === RectangleShape) {
            drawRect(color = this.color)
        } else { /* .. */ }

        drawContent()
    }
}
```
1. Modifier.then に ModifierNodeElement の BackgroundElement の新しいインスタンスが渡されてます。
2. Modifier.Element の Background と同様で関数パラメータが BackgroundElement のコンストラクタパラメータとして渡されてます。
3. create で BackgroundNode の新しいインスタンスが返されてます。
4. BackgroundNode が DrawModifierNode に拡張されてるので、draw 関数が各 Recomposition で呼び出されます。
なので、update で BackgroundNode のプロパティが更新された後 `invalidateDraw` が明示的に呼び出す必要はないです。
5. 前の記事では記載しましたが、新しい Modifier.Node 仕組みで、Modifier.Element の比較される役割が ModifierNodeElement が持たせるようになった。
なので、equals メソッドを Background と似てる実装になってます。
6. 描画を調整するために Modifier に対応してる Modifier.Node クラスを DrawModifierNode に拡張されてます。
7. 関数パラメータが変わると、新しい値を使って再描画する必要がある。変更が可能として、Modifier.Node の color と shape インスタンスを `var` として保持されてます。
8. DrawModifierNode の `draw` メソッドが override され、Background と同じ実装が行われます。

***
引き続き[次の記事](https://qiita.com/_SUR4J_/items/d48372b5793c4a0fa65f)では、Modifier.composed の中で `@Composable` 関数を使って行われた実装を `@Composable` コンテキストがない Modifier.Node にどうやって書き換えるのかについていくつかチップスを紹介してます。興味あればぜひ読んでみてください。