Embracing AGP 9 and JDK 21: A Gradual Path to Android Build Optimization

As Android developers, major AGP (Android Gradle Plugin) updates are always significant. AGP 9 in particular promises a stricter, faster build environment, moving away from ambiguous configurations.

Instead of waiting for its full release and then scrambling to fix issues, why not start preparing your project now, gradually aligning it with "AGP 9 standards" through your gradle.properties file?

 

🤔 Why JDK 21 and AGP 9 Now? (The Ultimate Synergy)

When transitioning to AGP 9, updating to JDK 21 isn't just a "requirement"; it's a powerful "booster" that dramatically enhances your development experience.

  • Performance Synchronization: JDK 21's improved resource management, including features like Virtual Threads, allows Gradle to fully leverage its parallel build capabilities, leading to more stable and efficient builds.
  • Language Specification Alignment: By targeting Java 21, you bridge potential gaps in type inference and bytecode generation in mixed Java/Kotlin projects, especially as Kotlin 2.x gains traction.
  • Precision R8 Optimization: AGP 9 is optimized to parse and transform class files generated by JDK 21. This means that even with stricter settings, R8 can more accurately understand modern code structures, reducing the need for excessive keep rules while safely shrinking code.

This combination offers the kind of seamless experience you get from pairing the latest OS with the latest CPU.

 

🤔 Prepare with gradle.properties: 10 Flags to Enable Today

The strategy is simple: enable one flag at a time, fix any errors that arise, and then move to the next. This iterative approach is the most reliable way to prepare for AGP 9.

1. Structure Enforcement (Clean Up Your Project)

  • android.uniquePackageNames=true: Prevents duplicate package names across modules, eliminating resource conflicts.
  • android.usesSdkInManifest.disallowed=true: Enforces placing minSdk, targetSdk, etc., in build.gradle instead of AndroidManifest.xml.
  • android.defaults.buildfeatures.resvalues=true: Explicitly controls the generation of resValue entries.

2. Build Speed Enhancements

  • android.enableAppCompileTimeRClass=true: Uses lightweight R classes during app compilation, significantly improving build times for large projects.
  • android.sdk.defaultTargetSdkToCompileSdkIfUnset=true: Automatically sets targetSdk to compileSdk if unspecified, preventing inconsistent behavior.
  • android.dependency.useConstraints=true: Utilizes Gradle's "Constraints" feature for dependency resolution, making library version management more robust.

3. Aggressive R8 / Optimization Settings (The Biggest Hurdle)

  • android.r8.strictFullModeForKeepRules=true: Enables R8's Full Mode. This maximizes optimization but requires precise keep rules for code that relies on reflection, potentially leading to crashes if not handled correctly.
  • android.r8.optimizedResourceShrinking=true: Employs a more advanced algorithm for removing unused resources, leading to smaller app sizes.

4. Next-Gen Defaults

  • android.builtInKotlin=true: Prioritizes AGP's built-in Kotlin support.
  • android.newDsl=false: Use this to maintain the current DSL while preparing for future changes.

 

🤔 Conclusion: One Flag at a Time for a Smoother Future

The AGP 9 update is akin to a major cleanup. Attempting it all at once can be overwhelming, but tackling it gradually makes it incredibly rewarding.

Why not start with android.uniquePackageNames=true? With each flag you enable, your project will move closer to a more modern, robust, and efficient build environment.

👉 AGP 9.0 移行ガイド:新旧コード比較で見るモダンビルド設定


Hilt Build Error on Kotlin 2.3.0: Provided Metadata instance has version 2.3.0 — Causes and Fixes Explained


error: [Hilt] Provided Metadata instance has version 2.3.0, while maximum supported version is 2.2.0.

This article explains the background of this error and introduces a new solution available since Dagger 2.57.

 

🤔 🧑🏻‍💻 1. Cause of the Error

This error occurs because kotlin-metadata-jvm, a library used internally by Dagger/Hilt, cannot understand the newer Kotlin metadata format (version 2.3.0).

Shading (Inshading) explained:

  • Shading means that a dependency is relocated and bundled inside another library’s JAR.
  • In earlier Dagger versions, kotlin-metadata-jvm was shaded (hidden) inside Dagger itself.
  • As a result, developers could not override or update it, even if Kotlin introduced a new metadata version.
  • This tightly coupled Dagger’s compatibility to a specific Kotlin version and forced users to wait for a Dagger release.

 

🤔 🧑🏻‍💻 2. What Changed in Dagger 2.57

Starting from Dagger 2.57, kotlin-metadata-jvm is unshaded (no longer hidden).

This means:

  • The dependency is now resolved normally via Gradle
  • Developers can explicitly specify a newer version without waiting for a Dagger update

This architectural change significantly improves Kotlin version agility.

 

🤔 🧑🏻‍💻 3. Solution: Explicitly Declare the Dependency

If you are using Kapt

Kapt runs through the Java compiler and is more sensitive to metadata incompatibility.


dependencies {
    // Add the latest metadata library to kapt
    kapt("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0-Beta1")
}

If you are using KSP

KSP is directly integrated with the Kotlin compiler, so this error is less likely.

If needed, you can still specify it explicitly.


dependencies {
    // Add to ksp configuration
    ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0-Beta1")
}

Recommended: Force the version globally

If multiple modules are affected, this is the most reliable approach.


configurations.all {
    resolutionStrategy {
        force "org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0-Beta1"
    }
}

 

🤔 🧑🏻‍💻 4. Summary

  • If you are using Dagger 2.57 or later, you do not need to wait for a new Dagger release.
  • When the error appears, explicitly add the latest kotlin-metadata-jvm to your kapt or ksp configuration.
  • In general, migrating to KSP is recommended due to better compatibility and performance.
  • Developers who want to adopt the latest Kotlin features early should definitely apply this setup.

👉 Upgrade kotlin-metadata-jvm to support Kotlin 2.3.0 · Issue #5001 · google/dagger


How dp/sp/px Conversion Works in Android

  • This code defines extension functions to convert between dp, sp, and px.
  • It relies on density (for dp) and scaledDensity (for sp) extracted from Android’s DisplayMetrics.
  • The goal is to keep UI elements visually consistent across devices with different screen densities.

 

🧑🏻‍💻 Why These Conversions Matter (Foundation)

Screen Density Model


+---------------------------------------------+
| density       → converts dp ↔ px            |
| scaledDensity → converts sp ↔ px (font size)|
+---------------------------------------------+

  • dp: density-independent pixels
  • sp: scale-independent pixels (respects user font size setting)
  • px: raw physical pixels

density and scaledDensity come from:


resources.displayMetrics

This ensures the UI scales correctly across devices.

 

🧑🏻‍💻 Key Conversion Logic

1. dp → px

Formula: px = dp × density


dpToPx(dp) = dp.value * density

2. dp → sp

( dp → px → sp )
Formula: sp = dp × density ÷ scaledDensity


dpToSp(dp) = (dp.value * density / scale).sp

3. px → dp

Formula: dp = px ÷ density


toDp(px) = (px / density).dp

4. px → sp

Formula: sp = px ÷ scaledDensity


toSp(px) = (px / scale).sp

5. sp → dp

Formula: dp = sp × scaledDensity ÷ density


spToDp(sp) = (sp.value * scale / density).dp

6. sp → px

Formula: px = sp × scaledDensity


spToPx(sp) = sp.value * scale

7. Type-Specific Extensions

The code also adds natural calling styles:


Dp.toPx(context)
Float.toDp(context)
TextUnit.toPx(context)
TextUnit.toDp(context)

These simply delegate to the Context converters and make the API flexible.

 

🧑🏻‍💻 Visualization — Full Conversion Map


+------------------+           +------------------+
|       Dp         | <------>  |        px        |
|   (dp.value)     |           |     (Float)      |
+------------------+           +------------------+
          |                               ^
          | dpToSp / spToDp               |
          v                               |
+------------------+           +------------------+
|       Sp         | <------>  |    scaled px     |
|  (TextUnit.sp)   |           |  (scaledDensity) |
+------------------+           +------------------+

 

🧑🏻‍💻 Notes & Caveats

  • scaledDensity changes when users adjust system font size.
  • Jetpack Compose usually hides px conversions, but you still need px for:
    • Custom drawing
    • Canvas operations
    • Bitmap sizing
  • Expert consultation recommended for deeply understanding DPI internals in OEM-modified environments.

 

🧑🏻‍💻 References

👉 dp / px / sp 完全相互変換
👉 DisplayMetrics  |  API reference  |  Android Developers
👉 各種のピクセル密度をサポートする  |  Compatibility  |  Android Developers


Kotlin StateFlow: value vs. update – Which One Should You Use?

When updating a MutableStateFlow, you have two options. Here is how to decide instantly.

 

🧑🏻‍💻 The Golden Rule

  • Use update { } if the new value depends on the current value (e.g., incrementing a counter, toggling a boolean).
  • Use value = if you are completely overwriting the state (e.g., setting a loading state, resetting data).

 

🧑🏻‍💻 Why does it matter?

Direct assignment (value = ...) is not thread-safe for "read-modify-write" operations.

If two coroutines try to update the state simultaneously using .value = .value + 1, you risk a Race Condition where one update is lost.

The update function is atomic. It uses a Compare-And-Set mechanism to ensure that updates happen sequentially and safely, even across multiple threads.

 

🧑🏻‍💻 Code Comparison

❌ Risky (Race Condition prone)


// If called concurrently, updates might be lost
_uiState.value = _uiState.value.copy(count = _uiState.value.count + 1)

✅ Safe (Thread-safe)


// Guarantees consistency
_uiState.update { it.copy(count = it.count + 1) }

✅ Safe (Overwrite)


// No race condition risk because we ignore the previous state
_uiState.value = UiState.Loading

 

🧑🏻‍💻 Summary

When in doubt, use update. It is safer by default and prevents subtle concurrency bugs.


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 の更新、どうしてこんなに面倒なの?