Modifier.Node を使いましょう (Part 1: なぜ必要になった)

この記事のシリーズでは [DroidKaigi 2023 で私が発表した内容](https://speakerdeck.com/_sur4j_/droidkaigi-2023-modifier-dot-node-woshi-imasiyou)をもとに、Compose v1.3.0 あたりから追加された `Modifier.Node`API について記載しましました。

|シリーズのコンテンツ|
|--|
|Part 1: なぜ Modifier.Node が必要になった (この記事)|
|Part 2: [Modifier.Node APIの紹介](https://qiita.com/_SUR4J_/items/8047da7f90ddb9433539)|
|Part 3: [Modifier.Node に移行する基本手順](https://qiita.com/_SUR4J_/items/f7ca26dff56a928dd004)|
|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 が初めて追加されたのは [Android Dev Summit 2022](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_L3n1j4ajHjJ6QccFUvW1u) の頃で、[Leland](https://twitter.com/intelligibabble)氏 がその改善の思考についてセッションがありますので、ぜひ見てみてください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/BjGX2RftXsU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen/>

# Compose ランタイムの Smart Recomposition
Compose の基本的な構成要素は `@Composable` 関数ですね。この `@Composable` 関数を「Composition」というツリーのようなデータ構造に追加されます。
![Screenshot 2023-09-06 at 8.43.56 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2966247/56beedae-b1f6-f60d-aae5-e0cf0197e9bf.png)
初フレームでは、Compose Runtime が Composition ツリーの各ノードに訪れて、`Box`, `Text`, `Button` のようなレイアウト`@Composable` 関数を呼び出します。呼び出された`@Composable`が発行するUIコンポーネントを使ってレイアウトが構築されます。

その後、次々のフレームからUIを最適化するために、**Compose ランタイムが Composition ツリーで「変更が行われたかどうか」のためツリー全体を観察します**。ランタイムが「変更が行われた」と判断したノードのみを再呼び出し（Recomposition）して、構築されたUIを更新します。

このように関数の Recomposition を選択的スキップできることを一般的に「Smart Recomposition」と呼ばれてます。 

:::note info
`Composable` 関数の Recomposition をスキップできるかどうかの判断については [`ComposableFunctionBodyTransformer`](
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt) に多くの情報が記述されております。この記事のため2点を知ることが必要だと思います：
- non-Unit 値を返す `@Composable` 関数の Recomposition をがスキップされない。つまり、各フレームで再呼び出されます。
- Unit を返す `@Composbale` 関数の場合は、渡されてるパラメータが変更されていない場合、Recomposition がスキップされます。

```kotlin
@Composable
fun a(): Int { 
    Log.d(.., "a")
    return /* .. */ 
}

@Composable
fun A(value: Int) { 
    Log.d(.., "A: $value")
    /* .. */
}

@Composable
fun B(value: Int) { 
    Log.d(.., "B: $value")
    /* .. */
}

var state by remember { mutableStateOf(10) }
Button(onClick = { state += 1 }){..}
a()
A(4)
B(state)

/*
 ボタンタップし、Recomposition を実行したら output は以下の通り：
 a
 A, value: 4
 B, value: 10

 a
 B, value: 11

 a
 B, value: 12
 ..
 */
```
:::

Recomposition について公ドキュメントで詳しく書いてありますのでぜひ見てみてください。

https://developer.android.com/jetpack/compose/mental-model?hl=ja#recomposition

# 旧 Modifier 仕組み
`Modifier` は Compose UI の重要なものの1つです。複数の Modifier を「連結」して `@Composable` UIコンポーネントの見た目、位置などを特定なプロパティを提供することができます。

```kotlin
// 赤色の背景と 16dp の padding
Box(modifier = Modifier
    .padding(16.dp)
    .background(Color.Red)
)
```
基本的に、Modifier.background のような関数が `Modifier.then()` というメソッドで Modifier.Element のオブジェクトをあるデ-タ構造に連結されます。

:::note info
Modifier.Element が連結されて作られるデ-タ構造を一般的に「Modifier チェーン」と呼ばれることもあります。
:::

```kotlin
interface Modifier {
    ..
    /**
     * A single element contained within a [Modifier] chain.
     */
    interface Element : Modifier { /* .. */ }

    /**
     * Concatenates this modifier with another.
     * Returns a [Modifier] representing this modifier followed by [other] in sequence.
     */
    infix fun then(other: Modifier): Modifier { /* .. */ }
}
```
例えば、上記の Modifier の例を以下のように表します。
```kotlin
/* Modifier.padding(16.dp).background(Color.Red) */

Modifier
    .then(Padding(16.dp))
    .then(Background(Color.Red))

class Padding(all: Dp): LayoutModifier { /* .. */ }
class Background(color: Color, ..): DrawModifier { /* .. */ }

// レイアウト調整する用の Modifier.Element
interface LayoutModifier: Modifier.Element { /* .. */ }

// 描画する用の Modifier.Element
interface DrawModifier: Modifier.Element { /* .. */ }
```
## Modifier チェーンの役割
旧 Modifier 仕組みで、この Modifier チェーンの２つの役割ありました。

### 特定なプロパティーを提供する
Modifier チェーンがレイアウトに特的なプロパティ（サイズ、色、タップイベント時の処理、フォーカス与えるなど）を提供しました。
![Screenshot 2023-09-06 at 8.40.08 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2966247/3d877169-9244-6bbe-cac2-4b2f8d742b2d.png)
例えば、Modifier.background でレイアウトの背後に背景を描画することができます。
```kotlin
fun Modifier.background(color: Color, ..)
    = this.then(Background(color = color, ..))
 
class Background(color: Color, ..): DrawModifier { 
    // draw を override して、UIコンポーネントのコンテンツの後ろに
    // 背景を描画すること定義してます
    override fun ContentDrawScope.draw() {
            drawRect(color = color, ..)
            drawContent()
    }
}
```
少し余談で、Droidcon Berlin'23で Modifier チェーンを定義する順次の重要さについてセッションがあります。興味あれば、ぜひ見てみてください。

https://www.droidcon.com/2023/07/31/chaining-jetpack-compose-modifiers-beyond-intuition/

### Modifier が「変更されたかどうか」の判断のため比較
スマートな Recomposition のため、レイアウト `@Composable` に渡される Modifier パラメータが変更されたかどうかの判断が必要でした。
変更された場合、LayoutNode に新しい Modifier を設定し Recomposition が行われます。
    
```kotlin
// ../androidx/compose/ui/layout/Layout.kt
@Composable
inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    // Modifier.Element のチェーン
    val materialized = currentComposer.materialize(modifier)
    ..
    ReusableComposeNode<LayoutNode>(
        factory = ..
        update = {
            // 前の materialized インスタンスと異なった場合、LayoutNode の Modifier に
            // 新しい materialized インスタンスを設定され、invalidate されます
            set(materialized, { this.modifier = it })
            ..
        }
    )
}
```

## Stateless Modifier
元々 Modifier.Element を state を保持させないように (stateless) 想定して作られたみたいです。なので、Modifier.then メソッドにいつもそれぞれの Modifier.Element の新しいインスタンスを渡させるように作られました。
```kotlin
Modifier.background(color: Color, ..) 
    = this.then(Background(color..)) // 毎回新しいインスタンス渡された

// color が val になってます
class Background(val color: Color, ..): DrawModifier
```

つまり、各フレームで新しい生成される **Modifier.Element インスタンスとUIコンポーネントの lifespan (寿命) が一致しなかった**。

Stateless だったので、もし内部的に state を持つようになっても、毎回新しいインスタンスが渡されたので、保持された state もリセットされてしまいます。

```diff_kotlin
class Background(val color: Color, ..): DrawModifier {
+    var isCircleShape = false // 毎回 false として初期化されてしまいます
    override DrawScope.draw() {
        val shape = if (this.isRoundRect) CircleShape else ..
        /* .. */
    }
}
```
## Stateful Modifier
リップルアニメーションやフォーカスのような、Modifierを内部的に state を持たせる (stateful) 、ユースケースが必要になりました。そのきっかけ、[`Modifier.composed{}`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt) が追加されました。
```kotlin
// ../androidx/compose/ui/ComposedModifier.kt
fun Modifier.composed(
    ..
    factory: @Composable Modifier.() -> Modifier
): Modifier
```
`@Composable` factory ラムダパラメータの中で Compose ランタイムを活用して state の保持することは大きなメリットでした。

例えば、
```kotlin
fun Modifier.toggleBackground() = composed {
    // 内部的な state
    var selected by remember { mutableStateOf(false) }

    this.clickable { selected = !selected }
        .background(color = if (selected) Color.Red else Color.Blue)
}
```
でも、Modifier.composed が追加されてパフォーマンスには大きく影響を与えてしまいました。

# Modifier.composed のパフォーマンス問題
Statefulな Modifier が必要になったため Modifier.composed が追加されましたが、それでパフォーマンス面でいくつかの問題が発生されました。

## 不必要な Recomposition
:::note info
`@Composable` 関数に `@Composable` ラムダが渡されれば、Compose ランタイムがそのラムダをキャッシュすることができます。
```kotlin
@Composable
fun Box(
    // contents ラムダがキャッシュされてます
    contents: @Composable BoxScope.() -> Unit
)
```
:::

Modifier.composed 自体は `@Composable` 関数ではないので、Compose ランタイムを活用して factory ラムダをキャッシュすることはできないです。つまり、Modifier.composed が返す [ComposedModifier](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt?q=ComposedModifier) が `equals()` で比較を常に失敗されました。
![Screenshot 2023-09-06 at 8.57.48 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2966247/04796020-d96c-69e1-f4fa-3ed0a7492f1d.png)
Modifier.composed が Modifier チェーンに追加された場合、前の Modifier チェーンと比較されて常に「等しいではない」と判断されます。
その結果は、UIコンポーネントに渡される Modifier が変更されてないにも関わらず、各フレームで**不必要な Recomposition が行われました**。

```kotlin
@Composable
fun A(modifier: Modifier = Modifier) {
    SideEffect { Log.d(.., "Recompose A") }
    Text(modifier = modifier, ..)
}

@Composable
fun B(modifier: Modifier = Modifier) {
    SideEffect { Log.d(.., "Recompose B") }
    Text(modifier = modifier, ..)
}

A(modifier = Modifier
        .background(Color.Green)
        .composed { this } // ただ Modifier.composed を追加しただけ
        .padding(4.dp)
)
    
B(modifier = Modifier
        .background(Color.Green)
        .padding(4.dp)
)
 
/*
 Recomposition を実行したら output は以下の通り：
 Recompose A
 Recompose B

 Recompose B

 Recompose B
 ..
 */
```

## Compositionツリーの観察コストの増加
`@Composable` factory ラムダは Modifier を返します。
```kotlin
// ../androidx/compose/ui/ComposedModifier.kt
factory: @Composable Modifier.() -> Modifier, ..
```
Smart Recomposition でも話しましたが、non-Unit 値を返す `@Composable` 関数の再呼び出しがスキップされないです。なので、各フレームで必ず factory ラムダが呼び出されます。

Modifier.composed の factory ラムダが `@Composable` コンテキスト持ってるので、その中で呼び出される `@Composable` 関数が Composition ツリーに追加されます。

上記の Android Dev Summit'22 セッションからのスライドで `Modifier.clickable` のようなよく使われる Modifier も Composition ツリーのサイズを大きくしてしまいました。
![スクリーンショット 2023-08-03 21.11.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2966247/31c6cd0b-8b0c-cf92-8825-0217cf16f90e.png)
![Screenshot 2023-09-06 at 8.51.02 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2966247/6a2c1c50-0015-1a02-30d5-daf8cb93c31e.png)
つまり、Modifier.composed で Compositionツリーが大きくなって、**Compose ランタイムのツリーを観察するコストが増加されました**。

## インパクト
よく見かけるアプリのユースケース「アイテム一覧をタップして詳細画面開く」を Composeに実装すると、

```kotlin
LazyColumn {
    items(models) { model ->
        ListItem(
            modifier = Modifier.clickable { /* 繊維する */ },
            model = model, ..
        )
    }
}
```
この実装をよく考えてみれば、タップ処理を行うためアイテムレイアウトに設定された Modifier.clickable が、先ほど記載した2つのパフォーマンス問題をリストのサイズの倍数で大きくしてしまいますね。
![clickable追加されただけでのインパクト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2966247/dd08284a-bf5c-ae5c-5d34-ad61576edf9f.png)
このような色んなユースケースで Modifier.composed が使われていて Compose ランタイムのパフォーマンスが落ちてしまいました。

# Modifier.composed 問題の解決
:::note info
[Introduce Modifier.Node APIs, refactor Modifier/Wrapper/Entity strategy](https://android-review.googlesource.com/c/platform/frameworks/support/+/2108724) というタイトルの Modifier.Node API 改修に向けて最初 change list があります。この change list の説明は以下のように記述されてます。

> Change Summary
===
The added experimental Modifier.Node APIs provide an abstraction that allows for state to be maintained on an instance that will be retained with the lifecycle of the layout node, and will be allocated per-layout-node and per-usage of the corresponding Modifier.Element that produced it.
> 
> Broadly speaking, this abstraction provides an alternative mechanism to produce stateful modifiers without relying on the mechanics of the Modifier.composed API.

気になったら、ぜひ見てみてください。
:::
新しい Modifier.Node 仕組みで Modifier チェーンがを2つのチェーンに分担されてます。
- Node チェーン
- Element チェーン
![Screenshot 2023-09-06 at 8.53.02 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2966247/bb38350d-c3ab-f89c-eabd-e068eb348d40.png)
次の記事でもこの2つについて書きましたが、２つのチェーンの違いを軽くまとめてみました。

| Node チェーン| Element チェーン|
|--|--|
|Modifier.Node が連結されて作られてます|ModifierNodeElement が連結されて作られてます|
|UIコンポーネントに**特定なプロパティを提供**します|UIコンポーネントに設定された **Modifier に変更があったかどうかの判断のために比較**されます|
|UIコンポーネントはずっとNode チェーンの1つのインスタンスを持ち続けられます|前の仕組みの Modifier.Element チェーンと同様で各フレームで新しいインスタンスが生成されます。|
|Modifier.Node がUIコンポーネントの寿命と一致していて **state を保持することができます**|毎回ModifierNodeElementの新しいインスタンスが生成されるので、**stateを持たせない**|

なので、前の仕組みで Modifier チェーンに持たされた役割を2つのチェーンに分けられて、Modifier.composed で発生した問題が解決されてます：
* 「Modifier に state を保持させる」ニーズを Modifier.Nodeという長生きさせたオブジェクトで満たせてます。
  State を保持するために利用された `@Composable` コンテキストも不要になってるので、Composition ツリーのサイズが増える問題を解決できてます。
* Modifierに変更があったかどうかの判断を ModifierNodeElement というオブジェクトに任せてます。
  ラムダオブジェクトより、class オブジェクトを比較することになっていて不要な Recomposition が避けられてます。

***
この `Modifier.Node` API については[次の記事](https://qiita.com/_SUR4J_/items/8047da7f90ddb9433539)で書いてみましたので良ければ読んでみてください。