Modifier.Node を使いましょう (Part 4: @Composable 関数の実装を Modifier.Node に書き換える)

[前の記事](https://qiita.com/_SUR4J_/items/f7ca26dff56a928dd004)では、Modifier を Modifier.Node に移行するための基本の手順と移行する時利用する Modifier.Node の Interface のいくつかを紹介しました。

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

この記事では、Modifier.composed を移行する時、遭遇する `@Composable` 関数の実装をどうやって Modifier.Node に書き換えるのかをいくつかのまとめたチップスとして紹介します。

:::note warn
[公式のベストプラクティスが将来に公開される予定](https://android-review.googlesource.com/c/platform/frameworks/support/+/2640131)なので、これから紹介するチップスは私の方でいろんな change list を調べながらまとめたものです。参考ベースのみにしていただけると嬉しい。

将来にベストプラクティスが公開されたら、この記事でリンクを貼っておきます。
:::

始まる前のこの記事で書いた内容のために役立つ 2つの Modifier.Node の Interface を紹介します。

# CompositionLocalConsumerModifierNode
[`CompositionLocalConsumerModifier`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt%3Bl=35?q=CompositionLocalConsumerModifierNode&sq=&ss=androidx/platform/frameworks/support) に拡張すると、`currentValueOf` メソッドを使って CompositionLocal の現在の値を取得することができます。

```kotlin
class TextFieldCoreModifier: CompositionLocalConsumerModifierNode, .. {

    private fun DrawScope.drawSelection() {
        val selectionBackgroundColor 
                = currentValueOf(LocalTextSelectionColors).backgroundColor
        ..
    }
}
```

:::note warn
currentValueOf が `onAttach` と `onDetach` のコールバックの間でしか使えることができないです。それ以外（例えば、`init{}`）呼び出すとエラーが発生されます。

```diff_kotlin
class SampleModifierNode: CompositionLocalConsumerModifierNode, .. {
    init {
        // エラー発生されます
-        val density = currentValueOf(LocalDensity)
    }

    override fun onAttach() {
        // ここで呼び出して大丈夫
+        val context = currentValueOf(LocalContext)
    }

    fun update() {
        // このメソッドが ModifierNodeElement.update で呼び出されたら
        // ここで呼び出して大丈夫
+        val view = currentValueOf(LocalView)
    }

    // メソッドが呼び出されるタイミングが明確ではなかったら、
    // 常に currentValueOf を呼び出す前に isAttached を確認してください。
    private fun getContentColor(): Color? {
+        if (isAttached) {
+            return currentValueOf(LocalContentColor)
+        }
        return null
    }
}
```
:::

# ObserverModifierNode
スナップショットオブジェクト（MutableState、CompositionLocal など）の読み込みは、Composeランタイムに観察されます。つまり、スナップショットオブジェクトの値が変更されると、その値が読み込まれる `@Composable` 関数の Recomposition が行われます。

しかし、Modifier.Node内のスナップショットオブジェクトの読み込みは、ランタイムに観察されてないため、値の変更が受信されないです。

Modifier.Node が [`ObserverModifierNode`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverModifierNode.kt) を拡張すると、observeReadsブロック内で読み込まれるスナップショットオブジェクトの変更で onObservedReadsChanged コールバックでが呼び出されます。

```kotlin
@Stable
class SampleState() {
    var value: Int by mutableStateOf(0)
        private set
}

class SampleModifierNode(state: SampleState) {
    override fun onAttach() {
        observeReads{
            val value = state.value // 'value'を読み込み
            ..
        }
    }

    override fun onObservedReadsChanged() {
        // SampleState.value が変更されたらこのコールバックが呼び出されます。
    }
}
```

## 継続的な観察
observeReads を使用すると、スナップショットオブジェクトの読み込みは1回だけ観察されます。つまり、オブジェクトの値が変更され、onObservedReadsChanged コールバックが呼び出された後観察が停止されます。
次の Recomposition時に値が変更しても onObservedReadsChanged コールバックが呼び出されない。

しかし、CompositionLocals のようなスナップショットオブジェクトは任意に変更される可能性があります。最新の値で更新されるためには、読み込みを継続的に観察する必要があります。
これは、onObservedReadsChanged コールバック内で observeReads を呼び出すことで継続的な観察することができます。

```kotlin
class SampleModifierNode: 
    CompositionLocalConsumerModifierNode,
    ObserverModifierNode, .. {

    // [1]
    override fun onObservedReadsChanged() {
        // [2]
        observeReads {
            val density = currentValueOf(LocalDensity)
            ..
        }
    }
}
```
1. observeReads の中で読み込まれたスナップショットオブジェクトが変更されたら、onObservedReadsChanged が呼び出されます。
2. onObservedReadsChanged の中で observeReads を使って LocalDensity のようなスナップショットオブジェクトが読み込まれてるので、次回 LocalDensity の値が変更されたらまた [1] の onObservedReadsChaged が呼び出され、ループ形式になります。

# `@Composable` 関数を Modifier.Node に書き換えるパターン
それではこの記事の本題、`@Composable` 関数の実装を Modifier.Node に書き換えるときのいくつかのパターンを紹介します。

## remember {..}
remember 関数は、Recomposition を超えて中身で保持されてるオブジェクトをキャッシュします。
Modifier.Node のインスタンスも Recomposition を超え LayoutNode と同じ寿命を持つようになってます。

なので、remember されてるオブジェクトを `val` として保持します。remember されてる `mutableStateOf` のインスタンスを「変更が可能」な state になってるため `var` として保持します。
:::

```diff_kotlin
fun Modifier.focusable() = composed {
-   var isFocused by remember { mutableStateOf(false) }
-   val bringIntoViewRequester = remember { BringIntoViewRequester() }
   ..
}

class FocusableNode: Modifier.Node(), .. {
+   var isFocused = false
+   val bringIntoViewRequester = BringIntoViewRequester()
   ..
}
```
:::note info
`remember { mutableStateOf(..) }` の場合 `var` より `mutableStateOf` として保持すると、ランタイムが `State<T>` の read write を観察してるので、値を設定された際 Recomposition が実行されます。
:::
`rememberUpdatedState` is a fancy remembered MutableState which always holds the latest values. So, they can be held as `var` instances and updated in `ModifierNodeElement.update()` method.

```diff_kotlin
fun Modifier.magnified(
    magnifierCenter: Density.() -> Offset,
) = composed {
-    val updatedMagnifierCenter by rememberUpdatedState(magnifierCenter)
}

class MagnifierElement(
   val magnifierCenter: Density.() -> Offset,
    ..
): ModifierNodeElement<MagnifierNode> {

    override fun create(): MagnifierNode {
        return MagnifierNode(
+           magnifierCenter = magnifierCenter,
+           ..
        )
    }

    override fun update(node: MagnifierNode) {
+        node.update(magnifierCenter, ..)
    }
}

class MagnifierNode(
+    var magnifierCenter: Density.() -> Offset,
+    ..
): Modifier.Node() {
    
     // ModifierElementNode.update() で呼び出す
+    fun update(magnifierCenter: Density.() -> Offset, ..) {
+        this.magnifierCenter = magnifierCenter
+        ..
+    }
}
```
## CompositionLocal の読み込み
CompositionLocal を読み込むために、上記の `CompositionLocalConsumerModifierNode` に拡張することと、CompositionLocal の最新の値を継続的に観察して読み込むために `ObserverModifierNode` を拡張します。



## DisposableEffect
`DisposableEffect` の仕様として、パラメータの `key` が変更される際まず `onDispose` ラムダーが実行されその後、メインブロックが実行されます。
```kotlin
@Composable
fun DisposableEffectSample(value: Int) {
    DisposableEffect(value) {
        Log.d(.., "main, value: $value")
        onDispose { Log.d(.., "onDispose, value: $value") }
    }
}

/*
  var value by remember { mutableStateOf(0) } 
  DisposableEffectSample(value = value)

  ボタンタップで value の値を +1 で変更したら output は以下の通り：
  onDispose value: 1
  main value: 1

  onDispose value: 2
  main value: 2

  onDispose value: 3
  main value: 3
*/
```
DiposableEffect の場合は、各 Recomposition で `key` のチェックが行われます。

Recomposition で Modifier が変更されたら ModifierNodeElement の update メソッドが 呼び出されますので、このメソッドで Modifier.Node の DisposableEffect の実装を行います。

```diff_kotlin
fun Modifier.focusable(
    interactionSource: MutableInteractionSource? = null, ..
) = composed {
    val focusedInteraction = remember { mutableStateOf<FocusInteraction.Focus?>(null) }
    
-    DisposableEffect(interactionSource) {
-        onDispose {
-            focusedInteraction.value?.let { oldValue ->
-                val interaction = FocusInteraction.Unfocus(oldValue)
-                interactionSource?.tryEmit(interaction)
-                focusedInteraction.value = null
-            }
-        }
-    }
    ..
}

class FocusableNode(
    var interactionSource: MutableInteractionSource?
): Modifier.Node() {
    private var focusedInteraction: FocusInteraction.Focus? = null

    // FocusableElement.update() で呼び出されてる
    fun update(interactionSource: MutableInteractionSource?) {
        // interactionSource が変更する場合に
+        if (this.interactionSource != interactionSource) {
+            // onDispose ブロックの実装
+            disposeInteractionSource()
+
+            /*
+             メインブロックの実装はここで行われます。
+             Modifier.focusable の場合はないので、そのまま進めます。
+             */
+
+            // 次のイテレーションで比較するために値を更新
+            this.interactionSource = interactionSource
+        }
    }

+    private fun disposeInteractionSource() {
+        interactionSource?.let { interactionSource ->
+            focusedInteraction?.let { oldValue ->
+                val interaction = FocusInteraction.Unfocus(oldValue)
+                interactionSource.tryEmit(interaction)
+            }
+        }
+        this.focusedInteraction = null
+    }
    ..
}
```

### LaunchedEffect
LaunchedEffect は DisposableEffect と似て、パラメータの `key` が変更されたらラムダーの中の coroutine が実行されます。
Modifier.Node で coroutine を実行するために[前の記事](https://qiita.com/_SUR4J_/items/d48372b5793c4a0fa65f#modifierfocusable-%E3%81%AE%E7%A7%BB%E8%A1%8C%E3%81%AE%E4%BA%8B%E4%BE%8B)で記載した Modifier.Node の `coroutineScope` を使います。
```diff_kotlin
fun Modifier.draggable(state: DraggableState) = composed {
-    LaunchedEffect(state) {
-        val interaction = DragInteraction.Start()
-        interactionSource.emit(interaction)
-        /* .. */
-    }
}

class DraggableNode(var state: DraggableState): Modifier.Node() {

    // ModifierNodeElement.update() で呼び出されるメソッド
    fun update(state: DraggableState) {
+        if (this.state != state) {
+            coroutineScope.launch {
+                val interaction = DragInteraction.Start()
+                interactionSource.emit(interaction)
+                /* .. */
+                
+                this.state = state
+            }
+        }
    }
}
```

### CompositionLocal が DisposableEffect の key
CompositionLocal の値を継続的に観察して、変更されたとき DisposableEffect が実行されるようなフローになりますので、`onObservedReadsChanged` コールバックで `observeReads` の中に DisposableEffect の実装を行います。

```diff_kotlin
fun Modifier.focusable() = composed {
-    val pinnableContainer = LocalPinnableContainer.current
-    DisposableEffect(pinnableContainer) {
-        pinHandle = pinnableContainer?.pin()
-        onDispose {
-            pinHandle?.release()
-            pinHandle = null
-        }
-    }
    ..
}

class FocusableNode(): Modifier.Node(),
+    CompositionLocalConsumerModifierNode,
+    ObserverModifierNode, 
    .. {
+    private var pinnedHandle: PinnableContainer.PinnedHandle? = null

+    override fun onObservedReadsChanged() {
+        val pinnableContainer: PinnableContainer? = null
+        observeReads {
+            pinnableContainer = currentValueOf(LocalPinnableContainer)
+        }
+        pinnedHandle?.release()
+        pinnedHandle = pinnableContainer?.pin()
+    }
    ..
}
```

### Disposable・LaunchedEffect の複数の key
複数の key の場合、key の中どれの一つでも変更されたら DisposableEffect や LaunchedEffect が実行されます。

Modifier.Node の中で上記のような実装を行う前に `if` チェックを入れて、保持されてれる state と update メソッドで新しい渡される値が state 毎に比較されて `||` (or) オペレーターで比較処理を連結します。

```diff_kotlin
fun Modifier.magnifier(..) = composed {
-    val view = LocalView.current
-    val density = LocalDensity.current

-    LaunchedEffect(view, density, ..){ /* .. */ }
}

class MagnifierNode(): Modifier.Node(), ObserverModifierNode, .. {
    var view: View? = null
    var density: Density? = null

    override fun onObservedReadsChanged() {
        observeReads {
+            val previousView = this.view
+            val view = currentValueOf(LocalView).also { 
+                this.view = it 
+            }
+            val previousDensity = this.density
+            val density = currentValueOf(LocalDensity).also { 
+                this.density = it 
+            }

+            if (previousView != view ||
+                previousDensity != density ||
+                ..
+            ) {
+                coroutineScope.launch { /* .. */ }
+            }
        }
    }
}
```

## 複雑な `@Composable` 関数
例えば、Modifier.tabIndicatorOffset を Modifier.Node に置き換えてみると、
```kotlin
fun Modifier.tabIndicatorOffset(tabPosition: TabPosition) = composed {
    val tabWidth by animateDpAsState(tabPosition.width)
    /* .. */
}
```
[`animateDpAsState`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt%3Bl=109?q=animateDpAsState&sq=&ss=androidx/platform/frameworks/support) という `@Composable` 関数があります。先ほど紹介したいくつかのチップスの `@Composable` 関数より、もっとロジック入ってる関数になっています。

```kotlin
@Composable
fun animateDpAsState(target: Dp) {
    val animatable = remember { Animatable(..) }
    LaunchedEffect(target) { /* .. */ }
    return animatable.asState()
}
```
このような複雑なロジックが入ってる `@Composable` 関数の場合は、まず関数を呼び出されてるところで、関数の代わりに内部実装を置き換えるように想像してみます。

```kotlin
fun Modifier.tabIndicatorOffset(tabPosition: TabPosition) = composed {
    // val tabWidth by animateDpAsState(tabPosition.width)
    val tabWidthAnimatable = remember { Animatable(tabPosition.width) }
    val tabWidth by tabWidthAnimatable.asState()

    LaunchedEffect(tabPosition.width) { /* .. */ }
    /* .. */
}
```
置き換えるように想像すると、上記のシンプルなパターンと似てるものが現れてますね。では、上記で紹介したチップスを利用して Modifier.Node に置き換えてみましょう。

```diff_kotlin
fun Modifier.tabIndicatorOffset(tabPosition: TabPosition) = composed {
-    val tabWidth by animateDpAsState(tabPosition.width)
    /* .. */
}

class TabIndicatorOffsetNode(
    var tabPosition: TabPosition
): Modifier.Node(), .. {
    // remember { Animatable(tabPosition.width) } の代わりに
+    val tabWidthAnimtable = Animatable(tabPosition.width)

    fun update(tabPosition: TabPosition) {
        // LaunchedEffect(tabPosition.width) の代わりに
+        if (tabPosition.width != this.tabPosition.width) {
+            coroutineScope.launch { /* LaunchedEffect ブロックの実装 */ }
+        }
+        this.tabPosition = tabPosition
        /* .. */
    }
}
```

# Modifier.focusable の移行の事例

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

https://android-review.googlesource.com/c/platform/frameworks/support/+/2450683/32/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt

差分を確認してみると、

```diff_kotlin
fun Modifier.focusable(
    interactionSource: MutableInteractionSource? = null, ..
) = composed {
    // [1]
-    var isFocused by remember { mutableStateOf(false) }

    // [2]
-    Disposable(interactionSource){ /* .. */ }

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

    // [4]
-    Modifier
-        .semantics { 
-            this.focused = isFocused
-            /* .. */ 
-        }
-        ..
-        .onFocusChanged { 
-            isFocused = it.isFocused
-            /* .. */
-        }
}

class FocusableNode(
    var interactionSource: MutableInteractionSource?
): Modifier.Node(),
    // [5]
+    SemanticsModifierNode,
+    FocusEventModifierNode,
+    CompositionLocalConsumerModifierNode,
+    ObserverModifierNode, .. {

    // [6]
+    var isFocused: Boolean = false

    // [7]
    // ModifierNodeElement.update で呼び出される
    fun update(interactionSource: MutableInteractionSource?) {
+        if (this.interactionSource != interactionSource) {
+            this.interactionSource = interactionSource
+        }
    }

+    // [8]
+    var pinnableContainer: PinnableContainer? = null
+    override onObserveRaadsChanged() {
+        observeReads {
+            val pinnableContainer = currentValueOf(LocalPinnableContainer)
+            if (this.pinnableContainer != pinnableContainer) {
+                /* .. */
+                this.pinnableContainer = pinnableContainer
+            }
+        }
+    }

    // [9]
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        // Modifier.semantics{..} ブロック内の実装
+        this.focused = this@TabIndicatorOffsetNode.isFocused
+        /* .. */
+    }
+    
+    override fun onFocusEvent(state: FocusState) {
+        // Modifier.onFocusChanged {..} ブロック内の実装
+        this.isFocused = state.isFocused
+        /* .. */
+    }
}
```
1. remember されてる isFocused の mutableState を FocusableNode で [6] で `var` として保持されてます。
2. パラメータが key になってる DisposableEffect を FocusableNode で [7] の通り、ModifierNodeElement の update メソッドで実装が行われてます。
3. 内部 CompositionLocal の LocalPinnableContainer が DisposableEffect の key になってます。なので、FocusableNode で [8] の通り、継続的な観察して保持されてる pinnableContainer と比較されて DisposableEffect の実装が行われてます。
4. Modifier.focusable は複数の Modifier に組み合わせて作られてます。なので、FocusableNode で [9] の通り、それぞれの Modifier の実装を複製しないといけないです。
5. Modifier.focusable が作られてる Modifier のそれぞれの Modifier.Node クラスが拡張されてる Modidifer.Node Interface.
そして、[8] で CompositionLocal の値を取得する用のCompositionLocalConsumerModifierと 値の変更を継続的に観察するために ObserverModifierNode にも拡張されてます。


***
この記事が Modifier.Node に `@Composable` 関数の実装を書き換えるため参考になったら嬉しいです。

複数の Modifier の実装を1つの Modifier.Node クラスに実装すると、クラスサイズも大きくなって super class のような状態になってしまうかもしれないです。
もし、実装をいくつかの依存してない部分に分けることが可能だったら、1つの Modifier.Node の役割を複数の Modifier.Node クラスに delegate（委任・任せる）ことができます。[次の記事](https://qiita.com/_SUR4J_/items/134fcd5a3bc26d01dd78)ではこの delegate パターンについて記載してます。気になったらぜひ読んでみてください。