SideEffect はJetpack Composeのcomposable関数で、成功した再コンポジションのたびに渡されたコードブロックを実行します。LaunchedEffectやDisposableEffectとは異なり、SideEffectはキーに紐づかず、クリーンアップブロックもありません。コンポーズの状態をレンダリング後に外部システムと同期するだけです。そのため、コールバック関数の更新、ViewPagerとの同期、Analytics SDKへのデータ送信に最適です。Android Developers Documentation (2025) によると、SideEffectはComposeが成功した再コンポジションを確認した後にのみ実行され、再コンポジションがスキップされた場合は実行されません。
重要なポイント
SideEffect はJetpack Composeで最もシンプルな副作用APIです。composableコンポーネントの成功した再コンポジションごとにコードブロックを実行します。「成功した」という言葉が重要です。Composeが再コンポジションが不要と判断した場合(たとえば、すべての入力パラメータが変更されておらず、結果が同じになる場合)、SideEffectは実行されません。これにより、同期ブロックはUIが実際に変更されたときだけ呼び出されます。
SideEffect の主な使用例は、Composeツリーの一部ではないオブジェクトとComposeの状態を同期することです。典型的な例としては、Legacy Viewシステムでのコールバック関数の更新、ViewPagerへの現在の状態の受け渡し、表示データが変更されたときのAnalytics SDKへのイベント送信、外部フォーマットでの更新を期待するマッピングSDKとの同期などがあります。
Android Developer Blog (2025) によると、SideEffectはしばしばrememberと組み合わせて使用されます。rememberはオブジェクト(コールバックなど)を保持し、SideEffectは依存関係が変更されるたびにそれを更新します。このパターンは、リスナーオブジェクトを受け入れ、更新時にそれらを再作成しないライブラリにとって特に重要です。SideEffectがないと、リスナーは現在の状態への古い参照を保持することになります。
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setZoom(zoomLevel)
mapView.updateMarkers(markers)
}
AndroidView(factory = { mapView })
}
SideEffect を理解するには、Jetpack Composeの実行フェーズを理解する必要があります。Composeはフレームごとに3つのフェーズを経ます。Composition(何を表示するか)、Layout(どこに表示するか)、Drawing(どのように表示するか)です。SideEffectはCompositionフェーズの最後に実行されます。すべてのcomposable関数が実行された後、Layoutフェーズの前です。これにより、SideEffectは再コンポジション後のすべての変数の最終状態を確認できます。
ライフサイクルにおけるこの位置づけには重要な利点があります。SideEffect は、内部で状態が変更されても無限の再コンポジションを引き起こしません。コンポジションの後に実行されるため、SideEffect内で行われた変更は次のフレームでのみ適用されます。これにより、composable関数の本体内での状態変更に典型的なサイクル(コンポジション内のsetStateが現在のコンポジションが終了する前に新しいコンポジションをトリガーする)を防ぎます。
もう一つの特徴は、SideEffect がキーによって最適化されないことです。どの特定の状態が変更されたかに関係なく、すべての再コンポジションで実行されます。より精密な制御が必要な場合(特定のパラメータが変更されたときのみ実行する)、キー付きのLaunchedEffectを使用するか、rememberを介して変更チェックでSideEffectをラップしてください。
// SideEffect optimized with remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }
SideEffect {
if (currentZoom != zoomLevel) {
map.animateToZoom(zoomLevel)
currentZoom = zoomLevel
}
}
// Without this check SideEffect would call animateToZoom
// on every recomposition, even if zoomLevel didn't change
SideEffect の最も一般的な実用的シナリオは、現在の状態をキャプチャするコールバック関数の更新です。Jetpack Composeではこれを「コールバックライフサイクル管理」と呼びます。問題は、Kotlinのラムダ式が変数を参照でキャプチャすることです。コールバックがある値で作成され、その後変数が変更されても、コールバックは古い値を使い続けます。
例を考えてみましょう。Android用のGoogle Maps SDKは、setOnCameraMoveListener()を介して OnCameraMoveListener オブジェクトを受け入れます。isTrackingEnabledをキャプチャするラムダを渡した場合、isTrackingEnabledが変更されてもラムダは更新されません。Maps SDKは古いデータで古いコールバックを呼び出し続けます。SideEffectはこの問題を解決します。再コンポジションのたびにリスナーを再設定し、SDKが常に最新の状態で現在のラムダを使用することを保証します。
Maps SDK for Android Documentation (2025) によると、GoogleはMapsとJetpack Composeを統合する際にまさにこのパターンを推奨しています。同様のアプローチは、WebView、VideoView、TextureView、およびsetメソッドを介してコールバックを受け入れるその他のViewベースのコンポーネントにも使用されます。SideEffectは、状態が変更されるたびにコールバックが最新であることを保証します。
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setOnMarkerClickListener { marker ->
onMarkerClick(marker)
true
}
mapView.isTrafficEnabled = isTrackingEnabled
}
AndroidView(factory = { mapView })
}
SideEffect のもう一つの重要なシナリオは、UI状態が変化したときに分析システムにイベントを送信することです。たとえば、ユーザーがCompose画面内のTabLayoutでタブを切り替えると、SideEffectは現在選択されているタブの状態をFirebase AnalyticsやAppsFlyerに渡すことができます。選択されたタブが変更されるたびに(再コンポジションが発生すると)、SideEffectは対応するイベントを送信します。
onClick や onTabSelected で直接イベントを送信する場合との違いは、SideEffectが任意のソースからの状態変更でトリガーされることです。ユーザーアクションだけでなく、プログラムによる変更、画面回転後の状態復元、Deep Linksも含まれます。これにより、SideEffectは変更ソースから独立したユニバーサルな同期メカニズムになります。
Firebase Best Practices (Google, 2025) によると、SideEffectを介した分析イベントの送信は、ユーザージャーニーのより完全な画像を提供します。直接のユーザーアクションなしで発生するものを含むすべての状態変更をキャプチャするためです。ただし、やりすぎには注意が必要です。各分析イベントはネットワークリクエストであるため、頻繁に変化する状態(スクロール位置、指の座標)にはSideEffectは適していません。デバウンスを使用するか、重要な変更があった場合にのみイベントを送信してください。
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
SideEffect {
val params = Bundle().apply {
putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
putString(FirebaseAnalytics.Param.ITEM_ID, productId)
}
firebaseAnalytics.logEvent( FirebaseAnalytics.Event.VIEW_ITEM, params)
}
// UI with TabRow and selected tab
}
SideEffect と LaunchedEffect の選択は、2つの要因に依存します。非同期実行が必要かどうか、およびキーベースの制御が必要かどうかです。SideEffectは同期的で、すべての再コンポジションで実行されます。LaunchedEffectは非同期(コルーチン)で、キーが変更されたときにのみ実行され、再コンポジションのたびには実行されません。
| 特性 | SideEffect | LaunchedEffect |
|---|---|---|
| 実行 | 毎回の再コンポジション | キー変更時 |
| 非同期 | 同期 | コルーチン |
| キー | なし | あり (vararg) |
| クリーンアップ | なし | 自動コルーチンキャンセル |
| 典型的な使用法 | コールバック、Analytics、ビュー同期 | 読み込み、Flow購読、タイマー |
実際には、副作用の使用例の70%は LaunchedEffect(非同期操作、データ読み込み)、20%は DisposableEffect(クリーンアップ付きリソース)、そしてわずか10%が SideEffect(コールバック同期)でカバーされています。SideEffectは限られたタスクのための特殊なツールですが、それらのタスクでは不可欠です。
主な間違いは SideEffect内でComposeの状態を変更すること です。SideEffectは直接的に無限ループを引き起こしませんが(コンポジションフェーズの後に実行されるため)、過剰な再コンポジションを引き起こす可能性があります。SideEffect内で状態が変更されると(mutableStateOf)、次のフレームで新しい再コンポジションがトリガーされ、再びSideEffectが実行されます。これは無限ループではありませんが、フレームワークにとって不要な作業です。
2つ目の間違いは SideEffect内で重い計算を実行すること です。SideEffectはすべての再コンポジションで呼び出され、再コンポジションは1秒間に何十回も発生する可能性があるため(アニメーション、スクロール中)、SideEffect内の重いコードはフレームドロップの原因になります。重い操作はコンポジションの外に移動してください。コルーチン(LaunchedEffect)に移すか、derivedStateOf / rememberを介して計算してください。
3つ目の間違いは SideEffectを非同期コードに使用しようとすること です。SideEffectはsuspend関数ではないため、delay()、await()、collect()などのコルーチン操作はコンパイルできません。再コンポジション後に非同期アクションを実行する必要がある場合は、LaunchedEffectと組み合わせてsnapshotFlow { ... } を使用するか、rememberCoroutineScopeを介してコルーチンを起動してください。
よくある質問
はい、SideEffect は成功したすべてのコンポジションで実行されます。最初のコンポジションも含まれます。コンポーネントが最初に画面に表示されたときです。これはLaunchedEffect(Unit)とは異なります。LaunchedEffect(Unit)も最初のコンポジションで一度実行されますが、後続の再コンポジションでは実行されません(キーが変更されていない場合)。
いいえ、SideEffect はコンポジションフェーズの後に実行されます。内部で行われた変更は次のフレームでのみ適用されるため、ループを防ぎます。ただし、SideEffect内で頻繁に状態を変更すると、再コンポジションの連鎖が発生し、パフォーマンスが低下する可能性があります。SideEffect内での状態変更は、本当に必要な場合にのみ行ってください。
SideEffect はすべての再コンポジションで同期的に実行されます。snapshotFlowはComposeの状態からFlowを作成し、LaunchedEffectでcollectLatestとともに使用してリアクティブな変更処理ができます。snapshotFlowは、デバウンス、フィルター、またはdistinctUntilChangedを使用して変更に反応する必要がある場合に適しています。これは同期的なSideEffectでは不可能です。
Android Studio Compose Modifier Debugger を使用するか、コンポーネント名と呼び出し頻度をログに記録してください。SideEffectが想定よりも頻繁に実行される場合は、親コンポーネントの状態が不必要に変更されていないか確認してください。最適化:UIの安定した部分を個別のcomposable関数に分離し、unstableアノテーションを付けて再コンポジションの数を減らしてください。
はい、異なる目的で同じコンポーネント内で使用できます。DisposableEffect はリソースの設定とクリーンアップ(1回)を担当し、SideEffect は再コンポジションのたびにそのリソースと現在の状態を同期します。典型的な例:DisposableEffectがAPIを介してコールバックを登録し、SideEffectが変更のたびにそのコールバック内のキャプチャデータを更新します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。