DEV Community

Cover image for Same Gradle dependency flagged across 14 modules? That's one convention plugin, not 14 problems.
Stefan Wärting
Stefan Wärting

Posted on

Same Gradle dependency flagged across 14 modules? That's one convention plugin, not 14 problems.

If you run the Dependency Analysis Gradle Plugin (DAGP from here on) on a real project for the first time, the buildHealth report will be long. That's expected. But scroll through it and you'll probably see the same piece of advice repeated across a lot of modules:

:feature:search -- Unused dependencies which should be removed:
  implementation("com.squareup.okhttp3:okhttp:4.12.0")

:feature:profile -- Unused dependencies which should be removed:
  implementation("com.squareup.okhttp3:okhttp:4.12.0")

:feature:settings -- Unused dependencies which should be removed:
  implementation("com.squareup.okhttp3:okhttp:4.12.0")

(and twelve more)
Enter fullscreen mode Exit fullscreen mode

The obvious reaction is to start deleting, module by module. Fourteen tiny PRs. Don't.

When the same dependency shows up as unused across a big slice of your module graph, the cause is usually that a convention plugin is adding it to every module that applies it. Not fourteen independent mistakes, one thing to fix.

What a convention plugin does here

In any serious Gradle project you don't configure every module by hand. Shared setup goes into a convention plugin (in buildSrc/ or a separate build-logic/ included build), and each module applies it with a single line: plugins { id("my-team.android.library") }. That plugin sets up Android, Kotlin, the toolchain, and a default set of dependencies.

Good architecture. Also means DAGP sees each module's effective dependency set, which is whatever the module itself declares plus whatever the convention plugin added, and can't tell which is which.

What that looks like

A stripped-down example:

// build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt
class AndroidLibraryConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) = with(target) {
        pluginManager.apply("com.android.library")
        pluginManager.apply("org.jetbrains.kotlin.android")

        dependencies {
            "implementation"("com.squareup.okhttp3:okhttp:4.12.0")   // the culprit
            "implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
            // other "useful for everyone" defaults
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If only two of your fourteen library modules actually call into okhttp, DAGP flags it as unused in the other twelve. And it's right to. But deleting implementation("com.squareup.okhttp3:okhttp:...") in those twelve modules does nothing, because the convention plugin keeps adding it back. You can try to subtract at each consumer, but that's whack-a-mole and defeats the point of having a convention plugin in the first place.

Three ways to actually fix it

Once you've noticed the source is your convention plugin, the fix is one of three:

  1. The dependency belongs on every module that applies the convention plugin, and a handful of outliers don't use it. Leave the convention plugin, add an opt-out (a DSL flag, a narrower variant of the convention plugin, or an exclude mechanism). Outliers opt out explicitly.
  2. The dependency belongs on some modules, not most. Move it out of the shared convention plugin. Either into a narrower variant for the modules that need it, or into per-module declarations where they belong.
  3. The dependency doesn't belong there at all. Delete it from the convention plugin. This is more common than you'd expect. Usually a leftover from a migration nobody finished cleaning up.

Which option you pick depends on how many modules actually use the dependency. A couple out of fourteen: case 2. Most of them: case 1. None: case 3.

About the PR

Whichever option you pick, the change lives in the convention plugin. That has a wide blast radius, because every module that applies the convention plugin gets affected. Keep the PR isolated from any other cleanup, so if something breaks you can revert cleanly without losing unrelated work.

Doing it this way has a nice property on a big project: one convention-plugin PR can make a dozen or more DAGP advice entries disappear at once. Much better time-per-fix ratio than fourteen separate module PRs.

The habit worth picking up

When I look at a buildHealth report now, the first thing I do is group the advice by dependency coordinate instead of by module. If the same com.example:library:version appears in three or more modules, I treat it as one question about the convention plugin, not N separate fixes. That single change in approach has done more for our Gradle cleanups than any tooling has.

On tooling, for what it's worth: I wrote a small Claude Code plugin that does this pattern detection and keeps the resulting PRs batched sensibly. One install command: https://github.com/premex-ab/claude-marketplace/tree/main/plugins/gradle-dagp. Not affiliated with Tony or autonomousapps, just a consumer of advice.json.

Thanks to Tony Robalik for DAGP. advice.json being a real, machine-readable artifact is what makes any of this possible.

Top comments (0)