[AI Coding] The Essence of Asynchronous Processing! Mastering Compose with LaunchedEffect and produceState

I used to rely heavily on LaunchedEffect for handling asynchronous operations in Compose,but lately, I’ve realized that produceState might allow for a cleaner and more concise implementation.

So, I asked several AI assistants for their opinions and compared their responses.

 

🤔 GitHub Copilot

  • LaunchedEffect: Suitable for asynchronous operations that modify externally managed states (e.g., ViewModel).
  • produceState: Best for generating and managing asynchronous data directly within Compose.
  • Choose LaunchedEffect for externally managed state, and produceState for internally managed state.


@Composable
fun ExampleScreen(viewModel: MyViewModel = viewModel()) {
    // LaunchedEffect: Update externally managed state asynchronously
    val externalState by viewModel.uiState.collectAsState()
    LaunchedEffect(Unit) {
        viewModel.loadData() // Update external state asynchronously
    }

    // produceState: Generate and manage asynchronous data inside Compose
    val internalState by produceState(initialValue = "Loading...", key1 = viewModel) {
        value = viewModel.loadDataSync() // Assign result to internal state
    }

    Column {
        Text("LaunchedEffect State: $externalState")
        Text("ProduceState State: $internalState")
    }
}

 

🤔 ChatGPT

  • Use LaunchedEffect when updating existing state through side effects.
  • Use produceState when treating the result of an async operation as a state itself.
  • “Generate” with produceState, “manipulate” with LaunchedEffect.



@Composable
fun UserScreen(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }
    LaunchedEffect(userId) { user = api.fetchUser(userId) } // Update existing state

    val userState by produceState<User?>(initialValue = null, key1 = userId) {
        value = api.fetchUser(userId) // Convert async result directly to state
    }

    Text("Launched: ${user?.name ?: "Loading..."}")
    Text("Produced: ${userState?.name ?: "Loading..."}")
}

 

🤔 Gemini

  • produceState: Ideal for exposing results of suspend functions or external data sources as reactive State for the UI.
  • LaunchedEffect: Tied to the composable’s lifecycle, suited for one-time async actions or UI event triggers (e.g., showing a snackbar).
  • State management: produceState handles state automatically, while LaunchedEffect requires manual updates via mutableStateOf.


@Composable
fun UserDataState(userId: String): State<User?> = produceState<User?>(
    initialValue = null,
    key1 = userId // Automatically restarts when key changes
) {
    value = fetchUserFromApi(userId)
}

@Composable
fun EventSnackbar(viewModel: MyViewModel) {
    val scope = rememberCoroutineScope() // Scope for showing snackbar
    LaunchedEffect(Unit) { // Runs once on first composition
        viewModel.uiEvents.collect { event ->
            // Receive UI event and trigger a side effect (e.g., show snackbar)
            scope.launch { SnackbarHostState.showSnackbar("Received event!") }
        }
    }
}

 

🤔 Summary

  • LaunchedEffect: Used when updating externally managed states (e.g., ViewModel) through asynchronous operations.
  • produceState: Used when generating and managing the result of asynchronous processing directly as state within Compose.
  • If the state is managed externally, use LaunchedEffect; if it’s internally generated, use produceState.
  • LaunchedEffect requires manual state updates, while produceState automatically manages state.
  • Use LaunchedEffect to update external state, and produceState to generate internal state.
  • LaunchedEffect manipulates existing state through side effects.
  • produceState creates new state from asynchronous results.
  • Operate state = LaunchedEffect / Generate state = produceState
  • produceState: Ideal for exposing asynchronous results as automatically managed State objects for the UI.
  • LaunchedEffect: Used to manually update existing states (e.g., ViewModel’s state) or trigger side effects such as showing a snackbar.
  • Where the state is managed: If it’s generated within Compose → use produceState.
  • Where the state is managed: If it’s controlled outside Compose (e.g., ViewModel) → use LaunchedEffect.

👉 【AIコーディング】非同期処理の極意!LaunchEffect と produceState の使い分けで Jetpack Compose を制す


Activity x Compose Lifecycle: The Complete Visual Guide 🚀

 

👨‍💻 Activity Lifecycle


Activity.onCreate() <- Activity is created

↓

Activity.onStart() <- Preparing to become visible on screen

↓

Activity.onResume() <- Foreground state (interactive)

↓

Activity.onPause() <- Partially visible

↓

Activity.onStop() <- No longer visible

↓

Activity.onDestroy() <- Activity is destroyed

 

👨‍💻 Compose Lifecycle


[First Composition]
  - Composable is evaluated, Compose tree is built
  - LaunchedEffect -> Runs once after the commit
  - SideEffect -> Runs after every commit
  - DisposableEffect -> onDispose is called upon disposal

↓

[Recomposition]
  - UI is re-evaluated in response to state or data changes
  - Only the necessary parts are recomposed (efficient update)
  - SideEffect and DisposableEffect are also re-evaluated during recomposition

↓

[Dispose]
  - Depends on the ComposeView's disposal condition
  - DisposableEffect's onDispose is executed
  - Disposal timing is determined by the ViewCompositionStrategy

 

👨‍💻 Activity x Compose Lifecycle


Activity.onCreate() setContent { ... } <- Sets the ComposeView

↓

[First Composition]
  - Evaluates Composables and builds the UI
  - LaunchedEffect -> Runs once after commit
  - SideEffect -> Runs after each commit
  - DisposableEffect -> Defines onDispose

↓

Activity.onStart()

↓

Activity.onResume()

↓

[Recomposition]
  - Re-evaluates necessary parts in response to state changes
  - LaunchedEffect is not re-executed (only if its key changes)
  - SideEffect / DisposableEffect are re-evaluated

↓

Activity.onPause()

↓

(ComposeView is retained)
  - UI becomes partially obscured
  - State is maintained within the Composition

↓

Activity.onStop()

↓

[Preparing for Dispose (Detection)]
  - ViewCompositionStrategy monitors disposal conditions

↓

Activity.onDestroy()

↓

[Dispose]
  - ComposeView is destroyed
  - DisposableEffect.onDispose() is executed

This is the general flow.

Here are the notes for each item:

Activity.onCreate() A lifecycle method called when an Android app's Activity is created.

    setContent { ... }: Sets the UI using Jetpack Compose. This sets a ComposeView as the Activity's content view. The Compose lifecycle begins.

  • First Composition The Composable functions set in setContent are evaluated for the first time, and the UI is built.

LaunchedEffect: An Effect used for asynchronous processing. Runs only once after the first composition.

SideEffect: Runs after every composition commit (the timing when UI changes are applied).

DisposableEffect: An Effect used for resource cleanup. The logic defined in onDispose is executed when the Composition is disposed.

Activity.onStart() A lifecycle method called just before the Activity becomes visible to the user.

Activity.onResume() The Activity moves to the foreground and becomes fully interactive with the user.

  • Recomposition Only the necessary Composable functions are re-evaluated in response to changes in State.

LaunchedEffect: It is not re-executed during recomposition. However, it will be re-executed if its key changes.

SideEffect: It is re-evaluated on every recomposition.

DisposableEffect: It is re-evaluated on recomposition, and the cleanup logic (onDispose) from the old Effect is called.

Activity.onPause() The Activity enters a paused state and becomes partially obscured.

ComposeView Retention: The UI is not destroyed; it is retained. The state is also maintained within the Composition, allowing it to be reused upon re-display.

Activity.onStop() The Activity is no longer visible. A time to prepare for cleaning up state and resources.

Preparing for Dispose (Detection) ViewCompositionStrategy: A mechanism that monitors the conditions under which the ComposeView should be disposed. It triggers the Composition's disposal based on the View's lifecycle.

Activity.onDestroy() The timing when the Activity is completely destroyed.

  • Dispose The ComposeView is destroyed: The UI and state are completely released.
  • DisposableEffect.onDispose(): The resource cleanup logic is called.

 

👨‍💻 Summary

The Jetpack Compose and Activity lifecycles are closely linked, with specific processes occurring at each stage, from UI initialization (setContent) to disposal (Dispose).

It is especially important to understand the differences between effects like LaunchedEffect, SideEffect, and DisposableEffect, and to manage them appropriately.

Furthermore, by efficiently reusing the UI in response to Activity state changes (like onResume or onPause) and cleaning up resources as needed, you can achieve stable application behavior.


Kotlin・KSP・Compose Compiler を安全に更新する Renovate 設定術(developブランチ運用編)

Jetpack Compose Compiler が Kotlin に強く依存していた時代を経て、
今では少しずつ依存関係が緩やかになってきました。
それでも Kotlin・KSP・Compose Compiler の3つは依然として密接に関係しており、
バージョンのズレひとつでビルドが崩壊するリスクがあります。

この記事では、develop ブランチをメインに運用しつつ、
それらを安全かつ一貫性を保って更新するための Renovate 設定を紹介します。

 

🧩 Kotlin・KSP・Compose Compiler の三位一体更新

Compose Compiler は Kotlin コンパイラと深く結びついて動作するため、
Kotlin のメジャーアップデートが入ると、それに対応した Compose Compiler が必要になります。

さらに、KSP(Kotlin Symbol Processing)も Kotlin バージョンに追随するため、
この3つは基本的に「セットで更新する」のが鉄則です。


{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "baseBranches": ["develop"],
  "packageRules": [
    {
      "groupName": "Kotlin, KSP and Compose Compiler",
      "groupSlug": "kotlin",
      "matchPackagePrefixes": [
        "com.google.devtools.ksp",
        "org.jetbrains.compose.compiler"
      ],
      "matchPackagePatterns": [
        "org.jetbrains.kotlin.*"
      ]
    },
    {
      "description": "Do not automerge without CI",
      "matchUpdateTypes": ["minor", "patch", "digest"],
      "automerge": false
    }
  ]
}

 

⚙️ 設定の意図を読み解く

この設定は、単に自動更新を行うだけでなく、
Kotlin 界隈の依存を安全に、かつチームの開発フローに合わせて管理することを意識しています。


baseBranches: ["develop"]

Renovate のデフォルトは main や master に対して PR を作りますが、
実際の開発フローでは「開発用ブランチ(develop)」に更新を入れたいケースが多いですよね。

"baseBranches": ["develop"] を指定しておくことで、
更新 PR が常に develop ブランチに向けて作成されるようになります。

本番リリース前にテストや検証を挟める安全設計です。

 

🔗 groupName: 三つ巴のアップデートを1つにまとめる

groupName は、関連する依存をひとまとめにするためのグループ名。
ここでは "Kotlin, KSP and Compose Compiler" としており、
3つのパッケージを同時に1つの PR にまとめてくれます。


"matchPackagePrefixes": [
  "com.google.devtools.ksp",
  "org.jetbrains.compose.compiler"
],
"matchPackagePatterns": [
  "org.jetbrains.kotlin.*"
]

この指定で次のような依存が同時更新対象になります:


- Kotlin (org.jetbrains.kotlin)

- KSP (com.google.devtools.ksp)

- Compose Compiler (org.jetbrains.compose.compiler)

以前は androidx.compose.compiler でしたが、
現在の Compose Multiplatform では org.jetbrains.compose.compiler に移行しているため、この設定がより正確です。

 

🚫 automerge: false の哲学

Renovate には更新を自動マージする機能がありますが、
Kotlin 系の更新はそれに向きません。

理由は単純で、CI でのビルド確認が欠かせないからです。


{
  "description": "Do not automerge without CI",
  "matchUpdateTypes": ["minor", "patch", "digest"],
  "automerge": false
}

「CI による確認を通らない限り、自動マージさせない」というルールです。

特に Kotlin の minor アップデートでは内部 API が変わることもあり、
Compose Compiler や KSP が対応していない可能性があります。

PR を作成したあと CI を通し、問題なければ手動でマージする。

これが最も安全な流れです。

 

🧭 運用のヒントとまとめ

この設定は、いわば「Kotlin エコシステム用 Renovate セーフティモード」。

自動化の恩恵を受けつつも、壊れやすい依存を慎重に扱うための現実的な妥協点です。

項目 意図
baseBranches 更新PRをdevelop向けにして安全確認を確保
groupName Kotlin / KSP / Compose Compiler を同時に更新
automerge: false CI確認なしでの自動マージを防止
matchPackagePrefixes / matchPackagePatterns 最新の Compose 構成(Compose Multiplatform 等)に対応

 

🪶 まとめ:安全第一の Renovate 運用へ

Renovate は「ただの自動更新ボット」ではなく、
チームのアップデート戦略をコード化できるツールです。

Kotlin、KSP、Compose Compiler のような密接な関係を持つ依存こそ、
グルーピングとマージ制御で慎重に扱うべき対象。

develop ベースで CI を通すこの設定は、
自動化と安全性のバランスを取る最適解のひとつと言えます。

👉 Renovate Docs