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.