【Jetpack Compose】再コンポーズの仕組みを理解する

# はじめに

こちらの書籍を読んで、Jetpack Composeの再コンポーズのしくみについて学んだことをまとめました

https://gihyo.jp/book/2024/978-4-297-14488-3

# 動作環境
Android Studio Koala | 2024.1.1 Patch 1
Apple M2 Pro

# サンプルコード

```MainActivity.kt
package com.example.sampleapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.sampleapplication.ui.theme.SampleApplicationTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            SampleApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    LayoutA(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun LayoutA(modifier: Modifier = Modifier) {
    LayoutB(modifier = modifier)
}

@Composable
fun LayoutB(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally, // 水平方向の中央揃え
        verticalArrangement = Arrangement.Center // 垂直方向の中央揃え
    ) {
        var count by remember { mutableIntStateOf(0) }
        Button(
            onClick = { count++ },
            modifier = Modifier.padding(bottom = 16.dp)
        ) {
            Text("ボタン")
        }
        Text("カウント: $count")
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    SampleApplicationTheme {
        LayoutA()
    }
}
```
ビルドして以下の画面になればOKです
中央のボタンを押すとカウントアップします

<img width=200 alt="Screenshot_20250205_091326.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/b17e4ba2-cc71-891d-b759-dc9c2764423c.png">


# 解説

## メモリ上のUI構造
サンプルコードは、LayoutAの下にLayoutBがあり、さらにその下にColumn、その配下にTextとButtonがあるという階層構造になっています。（さらにButtonの配下にTextがあります）
このコードを実行すると次のような、コンポーザブル関数の親子関係を表現する木構造がメモリ上に構築されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/4627f7b0-8cb9-08d4-9b9a-d0f52c6e9c9f.png)

## 状態の更新に伴うUIの更新

State（今回の場合、状態変数 count）は、Composeフレームワークによって監視され、変更があった場合フレームワークはその変化を検知し、再コンポーズ（UIの更新）を実行します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/056d4a66-24ed-fc35-4bc2-611163f3a873.png)


次の1行がStateに対応します。状態変数をラッパーすることでComposeフレームワークの監視対象にできると思ってもらえればいいと思います

``` var count by remember { mutableIntStateOf(0) } ```

ここでは、`Int`や`Float`などのプリミティブな変数をComposeフレームワークの監視対象にするため、`mutableIntStateOf`を使ってラップしています。引数で初期値を設定しています。

これで、count変数が変更されると、それに伴ってUIの更新処理が走るようになりました。これを再コンポーズと呼びます。

## 再コンポーズはなるべく小さい範囲で実行される

再コンポーズ（UIの更新処理）は、効率的な処理の観点からなるべく小さい範囲で実行されます。

### 再コンポーズの起点

- 基本的に再コンポーズの起点となるのはStateを保持しているコンポーザブル関数です
- ただし、そのコンポーザブル関数がinlineの場合は、その親のコンポーザブル関数が起点になります

:::note info
inline関数は、コンパイル時にその呼び出し元にコードが展開されるため、
実質的にその関数自体は存在せず、親のコンポーザブル関数の一部として扱われます。そのため、再コンポーズの起点は親のコンポーザブル関数となります。
:::

### 今回の場合

- Stateを持っているのはColumnですが、Columnはinline関数であるため、その親であるLayoutBが起点となります
- **つまり、LayoutAは再コンポーズの対象外となり、countが変化しても再描画されません**


## 再コンポーズのスキップ

効率化の観点から再コンポーズの対象範囲だったとしても、再描画されないコンポーザブル関数もあります

### スキップの条件

- コンポーザブル関数の引数が変化しないこと

### 今回の場合

- 再コンポーズごとに、コンポーザブル関数の前回の引数と今回の引数を比較して変化があれば再コンポーズを実行します
- つまり、`Text("カウント: $count")`はボタンタップ毎に引数が変化するため再コンポーズされ、`Text("このコンポーザブル関数はスキップされる")`はボタンタップ毎に引数が変化しないため、再コンポーズの対象から外れます
- このようにして Jetpack Composeは、効率的な再描画処理を実現しています


```skipped.kt
@Composable
fun LayoutB(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally, // 水平方向の中央揃え
        verticalArrangement = Arrangement.Center // 垂直方向の中央揃え
    ) {
        var count by remember { mutableIntStateOf(0) }
        Button(
            onClick = { count++ },
            modifier = Modifier.padding(bottom = 16.dp)
        ) {
            Text("ボタン")
        }
        Text("カウント: $count")
        Text("このコンポーザブル関数はスキップされる") // スキップ
    }
}
```
# まとめ

1. 状態変数をなるべく下の階層へ置くことで、再描画の範囲をコンパクトにできる
2. 差分がなければComposable関数の再実行はスキップされ、描画コストを抑えられる

このふたつの仕組みを使って、コンパクトな差分描画を実現！