Resource shrinking has been around forever, but it's always been... okay. You enable it, you get some size reduction, but there's always unused stuff that slips through.
Android Gradle Plugin 8.12 changes this. Google integrated resource shrinking directly into R8's optimization pipeline, and the results are noticeably better.
What is Android Resource Shrinking?
Android apps have two types of bloat: code and resources. They need different tools to clean up.
Code shrinking (R8/ProGuard) removes unused Kotlin/Java classes, methods, and fields from your compiled code. If you wrote a utility class but never call it, R8 strips it out.
Resource shrinking removes unused drawables, layouts, strings, and other XML/binary resources from the final APK. That splash screen you deleted from code but forgot to remove from res/drawable? Resource shrinking catches it.
The key insight: these two are connected. Dead code might reference resources, and unused resources might only exist because dead code references them. When you optimize them separately, both miss things. That's exactly what optimizedResourceShrinking fixes — it runs both in the same pipeline.
How R8 optimizedResourceShrinking Works
The traditional flow looked like this:
- AAPT2 compiles resources and generates keep rules
- R8 shrinks and optimizes code
- Resource shrinking runs as a separate post-processing step
The problem: AAPT2's keep rules were unconditional. It couldn't know which resources R8 would determine are unreachable. So it kept everything that might be used.
With optimizedResourceShrinking, R8 handles both in a single pass:
- R8 analyzes code reachability (which classes/methods are actually called)
- Simultaneously, it builds a resource reference graph (which resources are referenced by reachable code)
- Resources only referenced by dead code get removed along with that dead code
This means R8 can see the full picture. If a feature flag disables a module at compile time, both the module's code AND its resources get stripped.
Why the Old Approach Leaked Resources
Traditional resource shrinking worked as a separate step from code optimization. AAPT2 would generate keep rules, R8 would do its thing, and then resource shrinking would run. The problem: those AAPT2 keep rules were unconditional.
This meant:
- Code referencing unused resources stayed in the APK
- Resources only used by dead code stayed too
- Neither optimizer could see the full picture
The new approach runs both in the same R8 pipeline. Resources and code get analyzed together, so if code is dead, its resources go away too. And vice versa.
Enabling It
Add this to gradle.properties:
android.r8.optimizedResourceShrinking=true
You also need both minification and resource shrinking enabled in your release build:
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
That's it. Build your release APK and compare sizes.
What Kind of Reduction to Expect
Google claims 50%+ for apps with shared resources. In practice, it depends heavily on your app structure.
If you have a lot of library resources you don't use, or feature modules with resources only used in specific configurations, you'll see bigger gains. If your app is already lean and every resource is directly referenced, the improvement will be smaller.
Use APK Analyzer (Build → Analyze APK in Android Studio) to see where your size is actually coming from. Compare before and after - the res/ folder size should drop noticeably.
Watch Out For Dynamic Resource Loading
The biggest gotcha: resources loaded by string name at runtime.
// This will break - the optimizer can't see this reference
val resId = resources.getIdentifier("icon_$category", "drawable", packageName)
The optimizer doesn't know you're using those resources, so it removes them. Your app crashes with Resources$NotFoundException.
Fix it either by using direct references:
val resId = when(category) {
"food" -> R.drawable.icon_food
"travel" -> R.drawable.icon_travel
else -> R.drawable.icon_default
}
Or by adding a keep file at res/raw/keep.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/icon_*" />
ProGuard Rules for Resources
If you need to keep specific resources that are referenced dynamically:
# Keep resources referenced by name at runtime
-keep class **.R$drawable {
public static final int splash_logo;
public static final int app_icon_*;
}
Testing Your Build
After enabling optimized shrinking:
- Build a release APK
- Install and test the app thoroughly
- Pay special attention to features that load resources dynamically
- Test on different device configurations (screen densities, languages)
If something breaks, check your dynamic resource loading first. Nine times out of ten, that's the issue.
Common Problems With Resource Shrinking
After enabling this on multiple projects, here are the issues I've hit:
1. Reflection-based resource access
Libraries that load resources by name at runtime will break. This is the most common issue. If you use a library that dynamically constructs resource names (like some icon packs or theme engines), you need to add keep rules.
2. Library resources you don't use
Third-party libraries bundle their own resources — icons, layouts, strings. With optimized shrinking, R8 is more aggressive about removing these. Occasionally a library accesses its own resources through reflection, and shrinking breaks it. The fix is usually a ProGuard keep rule from the library's documentation.
3. WebView HTML referencing Android resources
If your WebView loads local HTML that references file:///android_res/drawable/image.png, the shrinking won't see that reference. Those resources get removed and your WebView shows broken images. Move these assets to assets/ instead of res/.
4. Multi-module resource conflicts
In multi-module projects, resources with the same name in different modules can cause unexpected behavior when shrinking. Use resource prefixes (resourcePrefix "module_" in build.gradle) to avoid collisions.
Before and After: Real Numbers
Here's what I measured on our APK Manager app (https://www.suridevs.com/blog/posts/apk-manager-backup-restore-guide/) after enabling optimized shrinking:
| Metric | Before | After |
|---|---|---|
| Total APK size | 25.1 MB | 13.2 MB |
res/ folder |
14.8 MB | 5.1 MB |
classes.dex |
6.2 MB | 4.9 MB |
| Unused drawables removed | — | 847 |
| Unused strings removed | — | 2,130 |
The biggest savings came from library resources we never used. Our app included Material Components, and the default theme bundles hundreds of drawables for widgets we never touch.
When It Becomes Default
Optimized resource shrinking is opt-in now, but it'll become the default in AGP 9.0.0. If you enable it now, you get ahead of any issues before you're forced to deal with them.
Is It Worth It?
For most apps, yes. The size reduction is real, and smaller APKs mean faster downloads and less storage pressure on user devices. The main cost is testing and potentially updating any dynamic resource loading patterns.
If you're distributing APKs directly (not through Play's AAB splitting), the savings are even more valuable since you're not getting per-device optimization anyway. For more on how APK packaging and split APKs work under the hood, see the Android PackageManager and ZIP64 APK guide at https://www.suridevs.com/blog/posts/apk-manager-backup-restore-guide/.
If you're building a Compose app and want to keep the overall architecture clean while optimizing, check out the MVVM Architecture guide at https://www.suridevs.com/blog/posts/mvvm-jetpack-compose-authentication-guide/.
Full technical details in Google's announcement.
Originally published at SuriDevs
Top comments (0)