SideEffect — Jetpack Composeにおける状態同期とは

著者: IT Sectr 公開日: 2026-06-30 読了時間: 9 分

SideEffect はJetpack Composeのcomposable関数で、成功した再コンポジションのたびに渡されたコードブロックを実行します。LaunchedEffectやDisposableEffectとは異なり、SideEffectはキーに紐づかず、クリーンアップブロックもありません。コンポーズの状態をレンダリング後に外部システムと同期するだけです。そのため、コールバック関数の更新、ViewPagerとの同期、Analytics SDKへのデータ送信に最適です。Android Developers Documentation (2025) によると、SideEffectはComposeが成功した再コンポジションを確認した後にのみ実行され、再コンポジションがスキップされた場合は実行されません。

重要なポイント

  • SideEffect — 成功した再コンポジションごとに実行されるコードのための副作用API。
  • 同期 — Composeの状態をComposeをサポートしない外部システムに渡します。
  • キーなし — LaunchedEffectと異なり、SideEffectは再起動せず、毎回の再コンポジションで実行されます。
  • クリーンアップなし — SideEffectはonDisposeを提供しません。一方向の同期専用に設計されています。
  • 同期的 — ブロックはコルーチンなしで、Composeのコンポジションフェーズ内で同期的に実行されます。

Jetpack ComposeにおけるSideEffectとは

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がないと、リスナーは現在の状態への古い参照を保持することになります。

kotlin
@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の仕組みとコンポジションフェーズ

SideEffect を理解するには、Jetpack Composeの実行フェーズを理解する必要があります。Composeはフレームごとに3つのフェーズを経ます。Composition(何を表示するか)、Layout(どこに表示するか)、Drawing(どのように表示するか)です。SideEffectはCompositionフェーズの最後に実行されます。すべてのcomposable関数が実行された後、Layoutフェーズの前です。これにより、SideEffectは再コンポジション後のすべての変数の最終状態を確認できます。

ライフサイクルにおけるこの位置づけには重要な利点があります。SideEffect は、内部で状態が変更されても無限の再コンポジションを引き起こしません。コンポジションの後に実行されるため、SideEffect内で行われた変更は次のフレームでのみ適用されます。これにより、composable関数の本体内での状態変更に典型的なサイクル(コンポジション内のsetStateが現在のコンポジションが終了する前に新しいコンポジションをトリガーする)を防ぎます。

もう一つの特徴は、SideEffect がキーによって最適化されないことです。どの特定の状態が変更されたかに関係なく、すべての再コンポジションで実行されます。より精密な制御が必要な場合(特定のパラメータが変更されたときのみ実行する)、キー付きのLaunchedEffectを使用するか、rememberを介して変更チェックでSideEffectをラップしてください。

kotlin
// 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を使ったコールバック関数の更新

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は、状態が変更されるたびにコールバックが最新であることを保証します。

kotlin
@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 })
}

Analytics SDKとの同期

SideEffect のもう一つの重要なシナリオは、UI状態が変化したときに分析システムにイベントを送信することです。たとえば、ユーザーがCompose画面内のTabLayoutでタブを切り替えると、SideEffectは現在選択されているタブの状態をFirebase AnalyticsやAppsFlyerに渡すことができます。選択されたタブが変更されるたびに(再コンポジションが発生すると)、SideEffectは対応するイベントを送信します。

onClickonTabSelected で直接イベントを送信する場合との違いは、SideEffectが任意のソースからの状態変更でトリガーされることです。ユーザーアクションだけでなく、プログラムによる変更、画面回転後の状態復元、Deep Linksも含まれます。これにより、SideEffectは変更ソースから独立したユニバーサルな同期メカニズムになります。

Firebase Best Practices (Google, 2025) によると、SideEffectを介した分析イベントの送信は、ユーザージャーニーのより完全な画像を提供します。直接のユーザーアクションなしで発生するものを含むすべての状態変更をキャプチャするためです。ただし、やりすぎには注意が必要です。各分析イベントはネットワークリクエストであるため、頻繁に変化する状態(スクロール位置、指の座標)にはSideEffectは適していません。デバウンスを使用するか、重要な変更があった場合にのみイベントを送信してください。

kotlin
@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 vs LaunchedEffect: 使い分け

SideEffectLaunchedEffect の選択は、2つの要因に依存します。非同期実行が必要かどうか、およびキーベースの制御が必要かどうかです。SideEffectは同期的で、すべての再コンポジションで実行されます。LaunchedEffectは非同期(コルーチン)で、キーが変更されたときにのみ実行され、再コンポジションのたびには実行されません。

特性SideEffectLaunchedEffect
実行毎回の再コンポジションキー変更時
非同期同期コルーチン
キーなしあり (vararg)
クリーンアップなし自動コルーチンキャンセル
典型的な使用法コールバック、Analytics、ビュー同期読み込み、Flow購読、タイマー

実際には、副作用の使用例の70%は LaunchedEffect(非同期操作、データ読み込み)、20%は DisposableEffect(クリーンアップ付きリソース)、そしてわずか10%が SideEffect(コールバック同期)でカバーされています。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は最初のコンポジションで実行されますか?

はい、SideEffect は成功したすべてのコンポジションで実行されます。最初のコンポジションも含まれます。コンポーネントが最初に画面に表示されたときです。これはLaunchedEffect(Unit)とは異なります。LaunchedEffect(Unit)も最初のコンポジションで一度実行されますが、後続の再コンポジションでは実行されません(キーが変更されていない場合)。

SideEffectは無限ループを引き起こす可能性がありますか?

いいえ、SideEffect はコンポジションフェーズの後に実行されます。内部で行われた変更は次のフレームでのみ適用されるため、ループを防ぎます。ただし、SideEffect内で頻繁に状態を変更すると、再コンポジションの連鎖が発生し、パフォーマンスが低下する可能性があります。SideEffect内での状態変更は、本当に必要な場合にのみ行ってください。

SideEffectとsnapshotFlowの違いは何ですか?

SideEffect はすべての再コンポジションで同期的に実行されます。snapshotFlowはComposeの状態からFlowを作成し、LaunchedEffectでcollectLatestとともに使用してリアクティブな変更処理ができます。snapshotFlowは、デバウンス、フィルター、またはdistinctUntilChangedを使用して変更に反応する必要がある場合に適しています。これは同期的なSideEffectでは不可能です。

SideEffectが頻繁に実行されすぎる場合のデバッグ方法は?

Android Studio Compose Modifier Debugger を使用するか、コンポーネント名と呼び出し頻度をログに記録してください。SideEffectが想定よりも頻繁に実行される場合は、親コンポーネントの状態が不必要に変更されていないか確認してください。最適化:UIの安定した部分を個別のcomposable関数に分離し、unstableアノテーションを付けて再コンポジションの数を減らしてください。

SideEffectをDisposableEffectと組み合わせることはできますか?

はい、異なる目的で同じコンポーネント内で使用できます。DisposableEffect はリソースの設定とクリーンアップ(1回)を担当し、SideEffect は再コンポジションのたびにそのリソースと現在の状態を同期します。典型的な例:DisposableEffectがAPIを介してコールバックを登録し、SideEffectが変更のたびにそのコールバック内のキャプチャデータを更新します。

まとめ

  • SideEffect — Jetpack Composeで成功した再コンポジションごとに実行される同期コードのための副作用API。
  • 同期 — 主な使用例:Composeの状態を外部システム(Google Maps、WebView、ViewPager、Analytics SDK)に渡す。
  • コールバック — SideEffectは、現在の状態をキャプチャするコールバック関数がUIの更新ごとに最新であることを保証します。
  • ループなし — コンポジションフェーズの後に実行されるため、SideEffect内での状態変更は無限の再コンポジションを引き起こしません。
  • 制限事項 — キー、非同期、クリーンアップブロックをサポートしません。これらのタスクにはLaunchedEffectまたはDisposableEffectを使用してください。
  • パフォーマンス — SideEffect内での重い計算は避けてください。すべての再コンポジションで実行されます(最大毎秒60回)。
  • デバッグ — Compose Debuggerで呼び出し頻度を制御し、rememberを使用して不要な再コンポジションをフィルタリングして最適化してください。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください