Why are updates to Kotlin, Compose, and KSP such a hassle?

In Android development, you're constantly dealing with the same set of three: Kotlin, the Compose Compiler, and KSP.

They seem like a friendly group, but their update schedules are always completely different! You upgrade Kotlin, and the Compose Compiler isn't compatible. You change something, and KSP throws a build error because of an internal API change...

To better manage this "dependency triangle" situation, the main idea is to use Renovate's configuration to treat them as a single unit. The simple plan is: "Raise all Kotlin ecosystem dependencies at the same time!"

 

🧑🏻‍💻 Brief overview of the renovate.json file


{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:base",
    "group:all",
    ":dependencyDashboard",
    "schedule:daily"
  ],
  "baseBranches": ["main"],
  "commitMessageExtra": "{{{currentValue}}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}",
  "packageRules": [
    {
      "matchPackagePatterns": ["androidx.compose.compiler:compiler"],
      "groupName": "kotlin"
    },
    {
      "matchPackagePatterns": ["org.jetbrains.kotlin.*"],
      "groupName": "kotlin"
    },
    {
      "matchPackagePatterns": ["com.google.devtools.ksp"],
      "groupName": "kotlin"
    }
  ]
}

👉 architecture-samples/renovate.json at main · android/architecture-samples

Roughly summarized, here are the key points:

  • groupName: "kotlin" to bundle dependencies This setting specifies that the three elements—the Compose Compiler, Kotlin, and KSP—should be treated as belonging to the "same group." This allows Renovate to update them all together at once.
  • schedule: daily for a calm update pace This checks for updates once a day. You'll receive pull requests (PRs) on a daily basis, preventing a huge influx of dependency updates all at once, which makes things much easier to manage.
  • commitMessageExtra to see changes at a glance The version difference, like "2.0.10 → 2.0.20," is automatically added to the PR title. It's a small tweak, but surprisingly useful.

Setting up your configuration this way significantly reduces the tragedy of "Kotlin got updated, but Compose broke..."

 

🧑🏻‍💻 What We Found While Using It

Once this setup is in place, you can feel much more confident testing updates for everything Kotlin-related. Renovate diligently checks daily, automatically creating a PR whenever a new version drops.

But there's one small warning:

The Compose Compiler sometimes takes a little extra time to catch up to the latest Kotlin version. So, don't just merge the PR when you see it—it's highly recommended to verify the CI status first.

KSP is similar; because it depends on Kotlin's internal workings, it's safer to update it along with Kotlin and run your tests together.

 

🧑🏻‍💻 Summary: Teach Renovate that "These Three Are a Set"

The configuration we discussed treats the trio of Kotlin, the Compose Compiler, and KSP as a single group.

  • Bundle all Kotlin-related dependencies for simultaneous updates.
  • Check for updates at a manageable daily pace.
  • See version differences directly in the PR title.

Just implementing this significantly reduces the problems caused by versions getting out of sync and breaking your build.

💡 Key Takeaway: Use Renovate less as an "automatic update tool" and more as a "dependency rulebook."

We simply need to tell Kotlin, Compose, and KSP to cooperate and "work together."

👉 Kotlin・Compose・KSP の更新、どうしてこんなに面倒なの?


[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.onStart() までに終わる

 

🤔 Activity x Compose のライフサイクル


Activity.onCreate()
  setContent { ... }   ← ComposeView をセット

    ↓

  [初回コンポーズ(First Composition)]
    ・Composable を評価し UI を構築
    ・LaunchedEffect → コミット後に一度実行
    ・SideEffect → 各コミットごとに実行
    ・DisposableEffect → onDispose 定義

    ↓

Activity.onStart()

    ↓

Activity.onResume()

    ↓

  [再コンポーズ(Recomposition)]  
    ・状態変更に応じて必要部分のみ再評価
    ・LaunchedEffect は再実行されない(Key 変更時のみ)
    ・SideEffect / DisposableEffect は再評価

    ↓

Activity.onPause()

    ↓

  (ComposeView は保持)
    ・UI は部分的に見えなくなる
    ・状態は Composition 内で維持

    ↓

Activity.onStop() 

    ↓

  [破棄準備(Dispose 検知)]
    ・ViewCompositionStrategy による破棄条件監視

    ↓

Activity.onDestroy()

    ↓

  [破棄(Dispose)]
    ・ComposeView が破棄される
    ・DisposableEffect.onDispose() 実行

 

🤔 まとめ

初回コンポーズは Activity.onStart() までに終わる。

👉 これだけでわかる!Activity × Compose のライフサイクル完全図解 🚀