Your App is 10MB Heavier Because of a Library You Didn't Even Know You Had

 A Forensic Guide to Auditing Transitive Dependencies, Pruning DEX Bloat, and Fixing Gradle Version Conflicts.

Your App is 10MB Heavier Because of a Library You Didn't Even Know You Had

Imagine you’re packing for a weekend trip. You pack a suitcase, but unbeknownst to you, every item you put in brings along its “friends.” You pack a toothbrush; it brings a tube of toothpaste, a rinsing cup, and a towel. By the time you reach the airport, your carry-on is overweight, and you’re paying extra baggage fees for things you never intended to use.

This is exactly what happens in Transitive Dependency Hell.

When you add a single line to your build.gradle.kts, you aren't just adding one library. You are inviting a family tree of dependencies into your project. If left unaudited, these "ghost" libraries inflate your DEX count, slow down build times, and cause runtime crashes when two libraries disagree on which version of a shared friend they want to hang out with.

The Result: After removing just three unused transitive dependencies and aligning OkHttp versions, one production app I worked on saw a 7.4 MB reduction in APK size and an 18% drop in DEX count — with zero functional changes.

The Ghost in the Machine: What is a Transitive Dependency?

Direct Dependency is one you explicitly declare, like implementation("com.squareup.retrofit2:retrofit:2.9.0").

Transitive Dependency is a library that Retrofit needs to function (like OkHttp). You didn’t ask for it, but you got it.

The Real-World Crash: The Glide vs. SDK Trap

A common scenario: You switch from Glide to Coil to save size. You remove the Glide line from Gradle, but the app crashes with a NoClassDefFoundError. Why? Because a third-party SDK (like a Customer Support chat tool) was secretly relying on your project to provide a specific version of Glide. By removing your direct declaration, the version the SDK required may have been omitted or changed via conflict resolution to a version that lacks a specific method.

Step 1: Visualizing the Tree of Chaos

To cut the fat, you have to see it. Android Studio’s “External Libraries” list is too noisy. Instead, use the Gradle terminal to see what actually ships in your final artifact.

The Power Command

Run this to see the tree for your production build:

./gradlew app:dependencies --configuration releaseRuntimeClasspath > deps.txt

How to read deps.txt:

  • The (*): This means the dependency is repeated elsewhere and Gradle has omitted the children to save space.
  • The ->: This shows version resolution. By default, Gradle picks the highest version requested in the graph.
  • Note on BOMs: If you use a Bill of Materials (like Firebase or Compose BOM), version resolution is governed by constraints rather than “highest-version wins.”

Step 2: The Scalpel — Using exclude

If you discover a library is pulling in an ancient version of a library or a duplicate you already have, you can use the exclude rule to prune the graph.

Kotlin DSL Example (build.gradle.kts)

dependencies {
implementation("com.datacrunch.android:analytics-sdk:5.2.0") {
// Exclude the SDK's version of OkHttp to prevent version conflict
exclude(group = "com.squareup.okhttp3", module = "okhttp")
}

// Explicitly provide the version your app actually uses
implementation("com.squareup.okhttp3:okhttp:4.11.0")
}

Note: Use this with caution. If the parent SDK calls a method that only exists in its specific version of OkHttp, you will face a NoSuchMethodError at runtime.

Step 3: Automation — The Dependency Analysis Plugin

Manually checking a 2,000-line text file is a chore. The Dependency Analysis Gradle Plugin is the gold standard for this. It identifies:

  • Unused dependencies: Libraries you declared but never actually used.
  • Used transitive dependencies: Libraries you use in code but “stole” from another library’s declaration.

Setup:

Add to your root build.gradle.kts:

plugins {
id("com.autonomousapps.dependency-analysis") version "1.28.0"
}

Run ./gradlew buildHealth to get a report of exactly what to delete or move to runtimeOnly.

Step 4: Constraints and Resolution Strategies

Sometimes exclude is too tedious. You can use a dependencySubstitution to force a specific version across the entire project, ensuring "one version to rule them all."

configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute(module("org.jetbrains.kotlin:kotlin-stdlib"))
.using(module("org.jetbrains.kotlin:kotlin-stdlib:1.9.20"))
}
}
}

🔍 The Audit Checklist: Red Flags

Keep an eye out for these signs of dependency bloat in your deps.txt:

  • Multiple image loaders: Seeing Glide, Coil, and Picasso in the same graph.
  • Analytics Bloat: SDKs pulling in their own massive networking stacks (like an old version of Volley).
  • Fragmented AndroidX: Different versions of the same androidx artifact causing compatibility headaches.
  • Test Leaks: Test-only libraries appearing in the releaseRuntimeClasspath.

Important Note: While R8/ProGuard can remove unused classes, it often cannot remove entire libraries that are referenced transitively. Dependency hygiene is your first line of defense.

🙋‍♂️ Frequently Asked Questions (FAQs)

Will removing a transitive dependency make my app faster?

Yes. It reduces the DEX count (the code the OS loads) and the runtime memory footprint, which can improve startup performance and stability on low-end devices.

How do I know if a library is safe to exclude?

There is no magic button. Exclude it, clean the project, and perform a smoke test on the features that rely on the parent library. Look for NoSuchMethodError or ClassNotFoundException.

Does this matter for App Bundles (AAB)?

Absolutely. While AABs optimize resources (images/languages) for specific devices, the compiled code (DEX) is usually delivered in full. Unnecessary dependencies increase the download size for everyone.

💬 A Question for the Readers

What is the “ghost” library that bloated your app the most? Have you ever found a library pulling in something absurd (like an entire testing framework or an old version of JUnit) into your release build? Let’s swap horror stories in the comments.

Video References

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.



Comments

Popular posts from this blog

No More _state + state: Simplifying ViewModels with Kotlin 2.3

Why You Should Stop Passing ViewModels Around Your Compose UI Tree 🚫

Is Jetpack Compose Making Your APK Fatter? (And How to Fix It)