Kotlin Unresolved Reference After Adding a Dependency
Sync Gradle, then check module and coordinates: most red imports come from a missed sync or a dependency in the wrong module..
20+ years shipping production backend systems. Notes here come from systems that actually shipped.
- ✓An Android project with Gradle (Kotlin DSL or Groovy)
- ✓Basic comfort editing app/build.gradle.kts
- ✓Android Studio with Gradle sync available
- A red import means Gradle hasn't delivered the artifact yet: sync the project before anything else
- The dependency must live in the module that imports it; implementation hides it from other modules
- Verify coordinates against Maven Central and catalog aliases for typos — one letter breaks resolution
- Check repositories in settings.gradle and turn off Gradle offline mode for new artifacts
- You'll clear stubborn cases with invalidate-caches plus version alignment of Kotlin, AGP, and Compose
Think of your project as a library that orders books from warehouses (repositories). Adding a dependency places an order — but the book arrives only with delivery (Gradle sync). Wrong branch (module), misspelled title (coordinates), unused warehouse (missing repo), or a truck that won't leave (offline mode) all end one way: you reach for the book and it isn't there. Check each delivery step in order instead of reordering the same book five times.
You added one dependency line, and now half your imports are red. Unresolved reference after adding a dependency is Android's most common five-minute problem that somehow eats entire afternoons. The causes form a short list — unsynced project, wrong module, implementation-vs-api visibility, a typo in coordinates, a missing repository, stale caches, version mismatches — but they all paint the screen the same angry red, so developers thrash between fixes at random.
The confusion is structural. Gradle resolution, Kotlin compilation, and IDE indexing are three separate systems, and any of them can be the one that's actually broken. You'll invalidate caches when the problem is a typo, or retype coordinates when the problem is offline mode. Each wrong guess costs a full sync cycle, and on large projects that's ten minutes a pop. Four wrong guesses and lunch is gone.
This article orders the checklist by probability so your first guess is usually right. You'll learn the sync-first ladder, the implementation-vs-api rule, how to bulletproof version catalogs, and when stale caches or version skew are really to blame. Follow it top to bottom and most red imports die in minutes.
Sync First: Delivering the Artifact Before Debugging Anything
Gradle sync is the delivery truck between your declaration and your code. Editing a build file only changes the order; sync executes it — resolving coordinates against repositories, downloading artifacts, and rebuilding the IDE's index of available classes. Until sync completes successfully, the import is red because the class genuinely isn't available yet. This sounds obvious, but skipping the sync (or missing its failure banner) accounts for a huge share of reports.
Run sync deliberately and read its output. Android Studio's elephant icon or the 'Sync Now' banner triggers it; the Build window shows per-artifact progress and the exact failure when resolution breaks. A success with red imports remaining means the problem moved downstream to indexing or version skew. A failure names names: the artifact string it couldn't find, the repository that refused it, the network error underneath. That text is the diagnosis — copy it before clicking anything else.
Make sync cheap so developers actually run it. Keep configuration cache enabled, avoid dynamic versions (2.+) that force re-resolution, and don't stack heavy configuration-time work (network calls in build files) that turns every sync into minutes. When sync is fast, the team's first reflex becomes 'sync and read the log' instead of 'guess and retype.' That reflex alone halves the lifetime of most dependency incidents.
implementation vs api: Visibility Across Modules
Multi-module projects add a visibility dimension that single-module apps never meet. implementation() means 'my private dependency' — the compiler and IDE hide it from downstream modules. api() means 'part of my public contract' — downstream modules inherit it. When :feature imports a type that only :network declares with implementation, the import is red despite the artifact existing in the build. Nothing is broken; the dependency is simply private to the wrong module.
The fix starts with ownership: every module declares what it imports. If :feature uses OkHttp types directly, :feature's build file should declare OkHttp — duplicating the line is correct, not wasteful, because it documents real coupling. Promote :network's declaration to api() only when the type flows through public signatures (function parameters, return types, exposed properties) that consumers must compile against. Default to implementation everywhere else for faster builds and cleaner boundaries.
Review dependency placement like API design. Each api() declaration is a promise that constrains future upgrades — changing it breaks consumers. Audit with ./gradlew :module:dependencies and look for modules compiling against transitives they never declared; each is a red import waiting for the day the upstream module switches to implementation. Explicit declarations make the graph honest and the reds explainable.
Version Catalog Typos and Coordinate Errors
Typos are the most embarrassing cause and among the most common. Artifact coordinates are three-part strings (group:name:version) that humans mistype constantly — transposed letters, wrong group (androidx vs android.support), version that doesn't exist. Catalog aliases add a second typo surface: the alias definition and its use must match exactly. The compiler can't distinguish 'wrong name' from 'missing library,' so both present as the same red import.
Version catalogs (gradle/libs.versions.toml) convert most of these into caught errors. Coordinates live once in the TOML file; build files reference type-safe accessors (libs.core.ktx) with IDE autocomplete. A misspelled alias fails in the catalog with a precise message instead of failing as a phantom missing class. Centralized versions also end the drift where :app uses Moshi 1.14 and :feature uses 1.15, producing duplicate-class warnings at best and runtime NoSuchMethodErrors at worst.
Adopt two habits around the catalog. First, copy coordinates from Maven Central's artifact page, never from memory or chat snippets — five seconds of verification beats thirty minutes of sync spirals. Second, pin exact versions and avoid dynamic (2.+) or snapshot ranges in release branches; dynamic versions resolve differently over time and turn green builds red with no code change. Dependabot or Renovate PRs against the catalog keep versions fresh without the chaos.
Stale Caches, Offline Mode, and CI-Only Failures
Sometimes the declaration is right and the machinery is stale. Gradle caches artifacts, metadata, and IDE indexes aggressively; a corrupted entry or an interrupted download can poison resolution long after the underlying problem cleared. Symptoms are distinctive: coordinates verified on Maven Central, repositories correct, offline mode off — yet sync insists the artifact is missing. A second machine building the same commit cleanly confirms the environment, not the code, is at fault.
Escalate in order of blast radius. ./gradlew :app:dependencies shows exactly what resolved and from where — look for FAILED lines naming the artifact and repository. --refresh-dependencies forces metadata re-download without wiping everything. Invalidate Caches plus clean rebuild is the heavy hammer for corrupted indexes; expect a long re-index and schedule it deliberately. Check offline mode and proxy settings before any hammer: a surprising number of 'cache corruption' cases are Gradle forbidden from reaching the network.
CI deserves its own checklist because it fails differently than laptops. Verify the CI image's repositories mirror settings.gradle, confirm the Gradle home is writable (read-only caches fake resolution failures), and ensure lockfiles or exact pins prevent dynamic-version drift between runs. Print the failing repository in CI logs on resolution errors — that single line routes the fix to infra versus code in seconds instead of hours.
Kotlin Plugin and Compiler Version Skew
The Kotlin Gradle plugin version shapes what the compiler understands — language features, codegen for Compose, synthetic accessors, and multiplatform expectations. When it drifts from the stdlib version or the Compose compiler version, references to generated code go red while hand-written imports stay white. The pattern is unmistakable once you've seen it: your code is fine, @Composable functions and injected constructors are red, and no dependency error appears anywhere.
The fix is the compatibility table, not guesswork. Google publishes the Compose compiler ↔ Kotlin mapping; JetBrains documents Kotlin ↔ AGP support ranges. Pick a tested trio, declare all three in the version catalog, and upgrade them as one commit. After switching, invalidate caches and run a full clean build — half-upgraded incremental state produces phantom errors that send you chasing ghosts.
Guard the trio in review. Any PR bumping Kotlin, AGP, or the Compose compiler alone gets a request to bump the others per the table. Record the working trio in a comment at the top of libs.versions.toml so the next upgrade starts from known-good state instead of archaeology. Boring version hygiene here prevents the weirdest-looking reds in Android development.
Keeping Red Imports Rare Across the Team
Make red imports rare by construction. Keep a short team runbook — sync, module, coordinates, repos, caches, versions — linked from the onboarding docs so every developer runs the same ladder instead of inventing their own. Add a CI job that builds with --offline after a normal build to catch undeclared transitives, and a second job on a clean agent that catches cache-dependent greens. Both are cheap and both catch what laptops hide.
Standardize the catalog aggressively. No string coordinates in module build files, no dynamic versions on release branches, Dependabot PRs reviewed weekly. When the catalog is the only place versions live, upgrades become single-file diffs with predictable blast radius, and typo-class errors collapse to near zero. New modules copy an existing build file's dependency block instead of writing declarations from scratch.
Finally, log the wins. Each time the ladder resolves an incident in minutes, note which rung fixed it in the PR or postmortem. After a quarter you'll have data showing where your team actually bleeds — usually one rung dominates — and you can automate exactly that (a sync reminder bot, a catalog lint, a CI repo check). Process improves fastest when it aims at measured pain.
CI Went Red on Every Module After a Read-Only Cache Change
- Read the full sync error: it names the failing repository and the real I/O cause behind 'could not resolve.'
- Environment changes (infra, images, mounts) break builds that no code change explains — check those first on red CI.
- Pinned versions plus a writable cache make resolution reproducible; dynamic versions make it weather-dependent.
api() in the upstream module. Sync and rebuild the single module.google() and mavenCentral() are listed. Check Gradle settings for offline mode (must be off for new artifacts) and verify proxy/VPN allows Maven traffic. Run ./gradlew :app:dependencies --configuration debugRuntimeClasspath to see the resolution attempt.| File | Command / Code | Purpose |
|---|---|---|
| app | dependencyResolutionManagement { | Sync First |
| network | dependencies { | implementation vs api |
| gradle | [versions] | Version Catalog Typos and Coordinate Errors |
Key takeaways
Common mistakes to avoid
5 patternsAdding the dependency to the wrong module's build file
implementation() in the module that uses it, sync, and confirm the import resolves. Promote to api() only when the type leaks into your module's public signatures.Using api() for everything 'to be safe'
implementation() everywhere; switch to api() deliberately when consumers need the types. You'll keep builds faster and avoid leaking transitive APIs you didn't mean to expose.Typos in artifact names, groups, or catalog aliases
Mixing incompatible Kotlin, AGP, and Compose compiler versions
Missing repository or offline mode blocking the download
Interview Questions on This Topic
A new import is red after adding a dependency. What's your fix order?
Frequently Asked Questions
20+ years shipping production backend systems. Notes here come from systems that actually shipped.
That's Kotlin. Mark it forged?
5 min read · try the examples if you haven't