第5回:SideEffect / DisposableEffect / LaunchedEffect の使い分け完全整理

― Compose の“副作用”を制御できないと設計は必ず壊れる ―

※ChatGPTを使用して記事を作成しています。

はじめに

Jetpack Compose を触り始めてしばらくすると、誰もがこの壁にぶつかります。

  • 「LaunchedEffect を置いたら無限に処理が走る」
  • 「SideEffect と何が違うのか分からない」
  • 「DisposableEffect はいつ使うの?」
  • 「とりあえず LaunchedEffect(Unit) している」

これはあなたの理解不足ではありません
Compose の「副作用(Side Effect)」は、従来の View システムには存在しなかった概念だからです。

この記事では、

  • 副作用とは何か
  • なぜ3種類もあるのか
  • どれを選ぶべきか
  • 間違えると何が起きるのか

失敗例 → 修正版 → 判断基準 の順で整理します。

1. そもそも「副作用」とは何か?

Compose の基本原則はシンプルです。

Composable は「状態 → UI」を返す純粋関数であるべき

しかし現実の UI では、次のような処理が必要になります。

  • ログを送る
  • API を呼ぶ
  • Toast を出す
  • センサーを登録する
  • リスナーを設定・解除する

これらは UIを返さない処理 であり、Compose の世界では すべて「副作用」 と呼ばれます。

👉 副作用を UI の描画と混ぜると、再コンポーズのたびに事故が起きる のが問題です。

2. なぜ3種類もあるのか?

Compose には副作用を扱うために、次の3つが用意されています。

API役割
SideEffect再コンポーズ後に毎回実行
LaunchedEffect条件付きで非同期処理を実行
DisposableEffect登録と解除をセットで管理

これは「用途が違う」からです。
1つで何でもできる設計にしなかった のが Compose の賢さです。

3. SideEffect:「毎回実行されていい処理」専用

❌ 失敗例:副作用の意味を誤解する

@Composable
fun SampleScreen(count: Int) {
    SideEffect {
        println("count = $count")
    }
}

これは一見問題なさそうに見えますが、再コンポーズのたびに必ず実行 されます。

  • count が変わったとき
  • 親が再コンポーズされたとき
  • Theme が変わったとき

👉 UIと無関係な再コンポーズでも実行される

✅ 正しい使いどころ

SideEffect は次の用途に限定すべきです。

  • Compose 外の状態と同期する
  • 毎回実行されても問題ない

例:

@Composable
fun SyncSystemUi(color: Color) {
    val systemUiController = rememberSystemUiController()

    SideEffect {
        systemUiController.setStatusBarColor(color)
    }
}

✔ UI描画後に実行
✔ 再コンポーズされても問題ない
✔ 状態同期目的

「毎回走って困らないか?」が判断基準です。

4. LaunchedEffect:「条件付きで一度だけ or 変化時に実行」

❌ 最も多い地雷:Unit指定

@Composable
fun SampleScreen() {
    LaunchedEffect(Unit) {
        viewModel.load()
    }
}

これは一見「1回だけ実行」に見えますが、

  • Composable が破棄 → 再生成
  • Navigation 戻り
  • Configuration Change

再実行されます

👉 「画面に入ったら1回だけ」は保証されません。

❌ 無限ループ例

@Composable
fun SampleScreen(state: UiState) {
    LaunchedEffect(state) {
        viewModel.fetch()
    }
}
  • fetch → state 更新
  • state 更新 → 再コンポーズ
  • LaunchedEffect 再実行

無限リクエスト完成

✅ 正しい使い方:トリガーを絞る

@Composable
fun SampleScreen(userId: String) {
    LaunchedEffect(userId) {
        viewModel.load(userId)
    }
}

✔ 明確な依存
✔ 値が変わったときだけ実行
✔ 意図がコードに現れる

LaunchedEffect は「何をトリガーにするか」がすべてです。

5. DisposableEffect:「登録と解除は必ずセット」

❌ 失敗例:解除し忘れる

@Composable
fun LocationScreen() {
    val manager = remember { LocationManager() }

    LaunchedEffect(Unit) {
        manager.start()
    }
}
  • 画面を離れても止まらない
  • リスナーが生き続ける
  • メモリリーク

✅ DisposableEffect の正解例

@Composable
fun LocationScreen() {
    val manager = remember { LocationManager() }

    DisposableEffect(Unit) {
        manager.start()

        onDispose {
            manager.stop()
        }
    }
}

✔ 開始と終了が明示的
✔ ライフサイクルと連動
✔ 非同期でなくてもOK

登録・解除がある処理は、必ず DisposableEffect

6. 3つの使い分けまとめ(実務基準)

🔍 判断フロー

  1. 登録・解除が必要?
    → はい → DisposableEffect
  2. 非同期処理?
    → はい → LaunchedEffect
  3. 毎回実行されても問題ない?
    → はい → SideEffect

📌 よくある用途別まとめ

やりたいこと正解
API呼び出しLaunchedEffect
Analytics送信LaunchedEffect
System UI同期SideEffect
センサー登録DisposableEffect
リスナー設定DisposableEffect
Toast表示LaunchedEffect

7. 副作用を間違えると設計が壊れる理由

副作用を間違えると、

  • 無限実行
  • メモリリーク
  • 画面遷移バグ
  • 再現しない不具合

が発生します。

そして厄介なのは、「動いているように見える」ことです。

Compose は高速なので、問題が潜伏したまま本番に出やすい

まとめ

Compose の副作用は、

  • 「いつ実行されるか」
  • 「何をトリガーにするか」
  • 「いつ破棄されるか」

明示的にコードで表現するための仕組みです。

API意味
SideEffect毎回同期
LaunchedEffect条件付き非同期
DisposableEffect登録と解除

この3つを正しく使い分けられるようになると、Compose の設計は一段階レベルアップします。

タイトルとURLをコピーしました