Skip to content
Fernando Prieto Moyano edited this page Dec 25, 2025 · 1 revision

Migration Lessons Learned: Kotlin 2.1.0 + Hilt 2.54 + AGP 8.7.3

Critical Lessons from Clean Architecture Migration

1. Kotlin 2.x Visibility Rules Are Stricter

Problem

// This pattern fails in Kotlin 2.x with Hilt:
internal class CompanyInfoRepositoryImpl @Inject constructor(...)

abstract class DashboardModule {
    @Binds
    internal abstract fun bindRepository(impl: CompanyInfoRepositoryImpl): CompanyInfoRepository
}

Error: 'public' function exposes its 'internal' parameter type

Solution

  • Implementation classes can be internal
  • But @Binds methods MUST be public (not internal)
  • Hilt's generated code needs public access to binding methods
// Correct pattern:
internal class CompanyInfoRepositoryImpl @Inject constructor(...)

abstract class DashboardModule {
    @Binds  // Public, not internal!
    abstract fun bindRepository(impl: CompanyInfoRepositoryImpl): CompanyInfoRepository
}

2. Hilt Multi-Module: All Dependency Modules Must Be Android Libraries

Problem

CRITICAL: If you have a module providing Hilt dependencies (with @Provides or containing types injected elsewhere), it MUST be an Android Library, not a pure Kotlin module.

// ❌ FAILS - Pure Kotlin module with Hilt dependencies
plugins {
    id("kotlin")  // Wrong for Hilt!
}

// Network module provides ApiService which is @Inject'ed in feature modules
// Hilt's kapt in app module can't see it!

Error: ComponentProcessingStep was unable to process '...' because 'prieto.fernando.core.network.ApiService' could not be resolved.

Solution

// ✅ WORKS - Android Library module
plugins {
    id("com.android.library")
    id("kotlin-android")
    id("kotlin-kapt")
}

android {
    namespace = "prieto.fernando.core.network"
    buildFeatures {
        buildConfig = true  // Required if using BuildConfig
    }
}

Why: Hilt's annotation processor needs all dependency types on the classpath during kapt processing. Pure Kotlin modules' classes aren't available the same way as Android Library modules during the app module's kapt phase.


3. Explicit Dependencies Required for Hilt Aggregation

Problem

Transitive dependencies aren't enough for Hilt's classpath aggregation.

// ❌ App only depends on feature-navigation, which depends on other features
dependencies {
    implementation(project(":feature-navigation"))  // Transitively gets dashboard & launches
}
// Hilt can't discover ViewModels in feature-dashboard or feature-launches!

Solution

// ✅ Explicit dependencies in app module
dependencies {
    implementation(project(":core-network"))        // Direct dep on network
    implementation(project(":feature-dashboard"))   // Explicit!
    implementation(project(":feature-launches"))    // Explicit!
    implementation(project(":feature-navigation"))
    implementation(libs.kotlinx.serialization.json) // If used in Hilt modules
}

Reason: Hilt's annotation processor needs to see all modules with @InstallIn, @HiltViewModel, etc., directly on the app's classpath during kapt.


4. K2 Kapt Support Required for Kotlin 2.1.0

Problem

w: Support for language version 2.0+ in kapt is in Alpha and must be enabled explicitly. Falling back to 1.9.
[WARN] Issue detected with dagger.internal.codegen.ComponentProcessor

Solution

Add to gradle.properties:

# Enable Kotlin 2.0+ support in kapt (Alpha but necessary)
kapt.use.k2=true
kapt.include.compile.classpath=false  # Best practice for kapt

# Share test components across modules
dagger.hilt.shareTestComponents=true

Note: K2 kapt is in Alpha, but it's required for Kotlin 2.1.0. Expect some warnings but it works.


5. Gradle 8.14+ and JDK Configuration

Problem

Invalid Gradle JDK configuration found.
Inconsistent JVM-target compatibility detected for tasks

Solution

Remove hardcoded JDK paths, enable auto-detection:

# gradle.properties
# ❌ Remove hardcoded paths:
# org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home

# ✅ Use auto-detection:
org.gradle.java.installations.auto-detect=true
org.gradle.java.installations.auto-download=false

# Remove obsolete JVM args for Java 17+:
# org.gradle.jvmargs=-Xmx4096m -Dkotlin.daemon.jvm.options=--illegal-access=permit
org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

Update gradle-wrapper.properties:

distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip

6. Version Compatibility Matrix

Working Combination

# gradle/libs.versions.toml
[versions]
kotlin = "2.1.0"
androidGradlePlugin = "8.7.3"
hilt = "2.54"
kotlinxCoroutines = "1.10.1"
kotlinxSerialization = "1.8.0"
compose = "1.7.6"
composeCompiler = "2.1.0"  # Must match Kotlin version!

[plugins]
kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

Critical:

  • Kotlin 2.x requires org.jetbrains.kotlin.plugin.compose plugin
  • Compose Compiler version MUST match Kotlin version
  • Remove manual kotlinCompilerExtensionVersion from build.gradle.kts

7. Compose Compiler Plugin Migration (Kotlin 2.x)

Old Way (Kotlin 1.x)

android {
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"  // Manual version
    }
}

New Way (Kotlin 2.x)

plugins {
    alias(libs.plugins.kotlin.compose.compiler)  // Plugin handles it!
}

android {
    // No kotlinCompilerExtensionVersion needed!
    buildFeatures {
        compose = true
    }
}

8. Repository Management Centralization

Problem

Build was configured to prefer settings repositories over project repositories 
but repository 'Google' was added by build file 'build.gradle.kts'

Solution

Move ALL repository declarations to settings.gradle.kts:

// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

// ❌ Remove from root build.gradle.kts:
// allprojects { repositories { ... } }

9. buildSrc Custom Plugins Configuration

Problem

When removing dependency management from custom plugins, ensure basic setup remains:

Solution

// buildSrc/src/main/kotlin/prieto/fernando/android/plugin/AndroidPlugin.kt
class AndroidPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        with(project) {
            pluginManager.apply("com.android.library")
            pluginManager.apply("kotlin-android")
            pluginManager.apply("kotlin-kapt")
            pluginManager.apply("io.gitlab.arturbosch.detekt")
            
            // Configure Android but DON'T add dependencies here!
            // Let each module manage its own dependencies
        }
    }
}

Lesson: Custom plugins for configuration only, not dependency management.


10. Hilt ViewModel Scope Requirements

Correct Structure

@Module
@InstallIn(ViewModelComponent::class)  // ViewModel-scoped dependencies
abstract class FeatureViewModelModule {
    @Binds
    @ViewModelScoped
    abstract fun bindUseCase(impl: UseCaseImpl): UseCase
}

@Module
@InstallIn(SingletonComponent::class)  // Singleton dependencies
abstract class FeatureDataModule {
    @Binds
    @Singleton
    abstract fun bindRepository(impl: RepositoryImpl): Repository
}

Key: Separate modules for different scopes. ViewModels and their direct dependencies in ViewModelComponent, repositories and network in SingletonComponent.


11. Test Module Compatibility

Problem

// shared-testing module (pure Kotlin)
dependencies {
    implementation(libs.androidx.arch.core.testing)  // ❌ Fails!
}

Error: Incompatible because this component declares a component, with the library elements 'aar'

Solution

// ✅ Android-specific test deps go in feature modules
// shared-testing should only have pure Kotlin test utilities
dependencies {
    implementation(libs.kotlinx.coroutines.test)
    implementation(libs.junit)
    implementation(libs.mockk)
    // No AndroidX testing libraries!
}

12. Resource References in Multi-Module Projects

Problem

// In feature-launches module referencing shared-ui resources
ErrorAnimation(R.raw.error_animation)  // ❌ Wrong R!

Solution

import prieto.fernando.shared.ui.R as SharedUiR

ErrorAnimation(SharedUiR.raw.error_animation)  // ✅ Correct!

Best Practice: Always use type-safe resource imports with aliases in multi-module projects.


13. Navigation 3 Custom Implementation

When implementing custom Navigation (not Jetpack Nav):

// Key components needed:
@Serializable
sealed interface NavKey {
    @Serializable data object Dashboard : NavKey
    @Serializable data object Launches : NavKey
}

class NavBackStack(initialBackStack: List<NavKey>) {
    var backStack by mutableStateOf(initialBackStack)
    // State management + serialization for process death
}

@Composable
fun NavDisplay(backStack: NavBackStack) {
    AnimatedContent(targetState = backStack.current) { key ->
        resolveNavKeyToContent(key).invoke()
    }
}

14. Gradle Cache and Clean Build Strategy

When Hilt Issues Occur

# Nuclear option that actually works:
rm -rf */build .gradle
./gradlew clean
./gradlew assembleDebug

Reason: Hilt's annotation processor sometimes caches stale metadata. Full clean forces regeneration.


15. Feature Module Self-Containment

Correct Structure

feature-dashboard/
├── domain/
│   ├── model/           # Domain models
│   ├── repository/      # Repository interfaces
│   └── usecase/         # Use cases
├── data/
│   ├── model/           # Data models
│   ├── mapper/          # Data ↔ Domain mappers
│   └── repository/      # Repository implementations (internal)
├── presentation/
│   ├── vm/              # ViewModels + Contract
│   └── ui/              # Screens + UiModels
└── di/                  # Hilt modules

Key: Each feature is self-contained with its own data + domain layers. No shared data or domain modules.


16. Common Build Errors and Solutions

Error Cause Solution
Cannot create an instance of ViewModel Hilt can't find dependencies Ensure all modules are Android Libraries + explicit deps
ComponentProcessingStep was unable to process Missing type on classpath Add explicit dependency in app module
'public' function exposes 'internal' type Kotlin 2.x visibility rules Make @Binds methods public
Build Type contains custom BuildConfig fields, but the feature is disabled Missing buildConfig flag Add buildFeatures { buildConfig = true }
Support for language version 2.0+ in kapt is in Alpha Kotlin 2.x without K2 kapt Add kapt.use.k2=true to gradle.properties

Quick Reference Checklist

For New Feature Module with Hilt:

  • Use com.android.library plugin (not kotlin plugin)
  • Add namespace = "your.package.name"
  • Add buildFeatures { buildConfig = true, compose = true }
  • Add explicit dependency in app module's build.gradle.kts
  • Keep @Binds methods public (not internal)
  • Use @InstallIn(ViewModelComponent::class) for ViewModel deps
  • Use @InstallIn(SingletonComponent::class) for singleton deps
  • Add .gitignore for the module
  • Create AndroidManifest.xml (can be minimal: <manifest />)

For Kotlin 2.1.0 Migration:

  • Update to Gradle 8.14+
  • Add kotlin-compose-compiler plugin
  • Remove manual kotlinCompilerExtensionVersion
  • Add kapt.use.k2=true to gradle.properties
  • Update all Hilt, Compose, Coroutines versions
  • Remove hardcoded JDK paths
  • Enable JDK auto-detection
  • Clean build everything: rm -rf */build && ./gradlew clean assembleDebug

Final Architecture Decisions

  1. Feature-First Architecture: Each feature module is self-contained with domain, data, and presentation layers
  2. No Shared data/domain Modules: Only shared UI components and test utilities
  3. Core Modules: Only for truly cross-cutting concerns (network, testing utilities)
  4. Explicit Over Transitive: Always declare direct dependencies explicitly
  5. Android Libraries Everywhere: All modules with Hilt dependencies must be Android Libraries

Performance Notes

  • K2 kapt is in Alpha but stable enough for production
  • Build times are acceptable with proper Gradle caching
  • Runtime performance is unaffected by these changes
  • Hilt annotation processing adds ~5-10s to clean builds

Known Issues

  1. Proguard Warnings: [WARN] Issue detected with dagger.internal.codegen.ComponentProcessor - These are benign, code generation still works
  2. K2 Kapt Alpha Warning: Expected, can be ignored
  3. Compose Navigation Deprecations: rememberSystemUiController is deprecated, migrate to EdgeToEdge when possible

Last Updated: December 25, 2025 Gradle Version: 8.14.3 Kotlin Version: 2.1.0 AGP Version: 8.7.3 Hilt Version: 2.54