• Skip to main content
  • Skip to primary sidebar

Phelela

  • Phelela
  • Build with me
  • Engineering Notes
  • Beyond Code
  • My GitHub

My Journey Into R8 on a Legacy Android Project

2. December 2025

Last week was… intense. About a month ago, I joined a new project, and it instantly pushed me out of my comfort zone. As an Android developer I live in Jetpack Compose. I breathe Compose. But this project? A full-blown legacy system built almost entirely on XML.

My task was to gradually bring Jetpack Compose into this older architecture — and one of the first priorities was enabling R8. If you’re wondering why this matters (and why it caused a few wild days), this article is for you.

What Is R8?

R8 is the official code shrinker and optimizer for Android, built into the Android Gradle Plugin. It reduces APK/AAB size, removes dead code, and optimizes the final DEX output. It essentially replaces the old ProGuard pipeline with a faster, DEX-aware solution that improves performance and trims away everything your app doesn’t actually use.

In short:
smaller builds, faster builds, fewer surprises at runtime.

Why R8 Matters in Legacy + Compose Projects

This particular project has been alive for years. Most of the UI still lives in XML, and a massive dependency graph keeps old modules around even when they’re no longer relevant. Once we added Jetpack Compose on top, the amount of generated code quickly grew — which is normal, but our release builds started to bloat.

R8 became essential. With it, we could finally remove unused XML-based UI, outdated modules, and extra Compose helper classes that we definitely didn’t want shipping to users.

The Problems I Ran Into (and There Were Many)

Introducing R8 wasn’t smooth at all. A lot of issues came from:

Missing or misplaced ProGuard rules

Some rules were old, in the wrong modules, or simply ignored. R8 then happily removed classes or renamed methods that were actually accessed via reflection — breaking serializers and runtime logic.

Unnecessary transitive dependencies

Legacy projects often pull in libraries nobody remembers. Some added their own rules or annotation processors, causing random build issues until I excluded or replaced them.

Annotation-processing edge cases

Older DI frameworks, event buses, or serializers generated classes dynamically. R8 couldn’t see them, so it removed what looked unused and caused runtime crashes.

Reflection and serialization failing completely

Reflection is basically invisible to R8, and a lot of core features relied on it. JSON deserializers, dynamic handlers, annotation-based serializers — all of these started failing after shrinking.
The only fix: track every reflectively accessed class and add explicit keep rules.

How I Learned to Debug R8 (Without Losing My Mind)

Debugging R8 is… an experience. What helped me most:

Check backend responses

Understanding how the server structures its JSON helped me find which model fields and serializers needed to survive minification.

Use R8 diagnostics early

Seeing what R8 removed and why saved me hours. The diagnostic output clearly showed which rules weren’t picked up.

Temporarily keep more to find the problem

If the app works with broad rules and breaks when they’re removed, it’s easy to narrow down where the issue lives.

Put rules in the right module

Sounds simple, but took me the longest to debug. The module structure of legacy projects can be… creative.

R8 Best Practices (Simple & Practical)

  • Minimize reflection and remember R8 can’t analyze it.
  • Put rules in the correct module so they’re actually applied.
  • Avoid overly broad rules that inflate your build.
  • Note Kotlin specifics, especially around serializers and annotations.
  • Let R8 remove unused Compose helpers and previews.
  • Regularly clean up transitive dependencies.
  • Use diagnostic output to understand R8’s decisions.
  • Check that serialization models stay intact after shrinking.
  • Align debug and release configs, so issues appear earlier.
  • Document why each rule exists — future you will appreciate it.

Conclusion

Working with R8 in a legacy Android app moving toward Jetpack Compose isn’t easy, but it’s one of the most valuable optimizations you can invest in. Understanding how R8 interprets your codebase — especially reflective classes, annotation processors, and older modules — gives you full control over what stays and what goes.

With the right configuration, R8 becomes more than a shrinker. It becomes a guardian of your architecture: faster builds, smaller artifacts, and fewer runtime surprises. And once Compose takes over more parts of the app, the payoff becomes even bigger.

in Engineering Notes

Reader Interactions

you may also like
Making Tech Decisions as a Solo Developer
I’ll Remember This – Or Not: The Power of Documentation
How to Organize and Manage Issues Efficiently in Software Development
Top Resources for Learning Kotlin: My Journey and Recommendations

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Primary Sidebar

Katarína Kováčová

Hi, I’m Katarina. I’m a mobile developer working with Android and iOS, and this blog is where I share things I learn, test, or find interesting in the world of mobile development.

I enjoy clear explanations, practical insights, and topics that make me think — a mix I try to bring into my writing as well.

Outside of tech, I like Sudoku, running, and reading thrillers and detective stories. I also cook and bake gluten-free, because I’m celiac, and experimenting in the kitchen has become part of my routine.

Welcome — hope you find something here that’s useful or inspiring.

My life in pictures

My kind of offline mode.
Old but still stealing the spotlight.
Chaos of ideas, order of pillows.
Where the world goes quiet for a moment.
Because every good day starts with a cookie.
2025 © Phelela
theme by soleilflare