― 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つの使い分けまとめ(実務基準)
🔍 判断フロー
- 登録・解除が必要?
→ はい →DisposableEffect - 非同期処理?
→ はい →LaunchedEffect - 毎回実行されても問題ない?
→ はい →SideEffect
📌 よくある用途別まとめ
| やりたいこと | 正解 |
|---|---|
| API呼び出し | LaunchedEffect |
| Analytics送信 | LaunchedEffect |
| System UI同期 | SideEffect |
| センサー登録 | DisposableEffect |
| リスナー設定 | DisposableEffect |
| Toast表示 | LaunchedEffect |
7. 副作用を間違えると設計が壊れる理由
副作用を間違えると、
- 無限実行
- メモリリーク
- 画面遷移バグ
- 再現しない不具合
が発生します。
そして厄介なのは、「動いているように見える」ことです。
Compose は高速なので、問題が潜伏したまま本番に出やすい。
まとめ
Compose の副作用は、
- 「いつ実行されるか」
- 「何をトリガーにするか」
- 「いつ破棄されるか」
を 明示的にコードで表現するための仕組みです。
| API | 意味 |
|---|---|
| SideEffect | 毎回同期 |
| LaunchedEffect | 条件付き非同期 |
| DisposableEffect | 登録と解除 |
この3つを正しく使い分けられるようになると、Compose の設計は一段階レベルアップします。

