Menu
R8 Optimized Resource Shrinking in AGP 9: When Static Analysis Meets Dynamic Access

R8 Optimized Resource Shrinking in AGP 9: When Static Analysis Meets Dynamic Access

by Nikolay Vlasov

The transition to Android Gradle Plugin (AGP) 9.0 introduced a significant shift in how resource shrinking works. When isShrinkResources = true is enabled in your build type, AGP now uses optimized resource shrinking. This mechanism tightly couples resource analysis with code analysis, removing any resources referenced only by unused code. While this leads to smaller APKs, it can also cause issues in projects relying on dynamic resource access—problems that might have gone unnoticed in previous versions. (Android Developers)

Why do resources disappear in Dynamic Feature Modules?

Modern resource shrinkers rely heavily on static analysis. If a resource is referenced directly in your code (e.g., R.raw.my_resource), the shrinker can easily track its usage. However, when you access resources dynamically using string names and runtime lookups, static analysis can no longer reliably prove the resource is needed. With AGP 9, this becomes even more pronounced as optimized resource shrinking is now part of the unified code and resource optimization pipeline. (Android Developers)

In projects using Dynamic Feature Modules (DFM), the base module often lacks compile-time dependencies on the feature's R classes, especially when modules are delivered on-demand via Play Feature Delivery. While the new resource shrinker has supported dynamic features for some time, this specific architecture is where "missing resource" issues most frequently occur due to dynamic lookups. (Android Developers)

Take, for example, a snippet from the Smart Directory Agent Center project. Loading a full-text search index is implemented as follows:

private fun getFtsId(): Int {
    return dynamicContextWrapper.getContextForFts().resources.getIdentifier(
        "fts",
        "raw",
        featuresManager.ftsModulePackage,
    )
}

To the resource shrinker, this call is a "black box." The resource name is passed as a string rather than a static reference. As a result, the build tools might incorrectly assume the resource is unused and remove it during the release build, leading to a Resources$NotFoundException at runtime. (Android Developers)

R8 Optimized Resource Shrinking
Optimized resource shrinking tightly couples code analysis with available resources.

The Evolution of Resource Shrinking in AGP

It is important to note that AGP 9.0 didn't reinvent resource shrinking from scratch; rather, it made its behavior more rigorous and predictable within the optimized pipeline. Precise resource shrinking was already enabled by default in AGP 8.3. In AGP 9.0, optimized resource shrinking is automatically triggered when isShrinkResources = true. For versions between 8.6 and 9.0, this behavior usually required the explicit flag android.r8.optimizedResourceShrinking=true. (Android Developers)

Be aware of the regression in AGP 8.9: release notes documented a resource shrinking bug that caused missing resources in dynamic feature modules. This was later fixed in versions 8.9.2 and 8.10. If you encounter a crash in your release build, it might not be an architectural flaw—it could be a bug in a specific version of the shrinker. (Android Developers)

The Real Solution: Keep files and Explicit Resource Retention

The correct way to protect resources used indirectly is to define a keep file in your project's resources. Documentation recommends creating an XML file in res/raw/ (e.g., res/raw/my.package.keep.xml) rather than relying on a generic "global keep.xml" as a special mechanism. Using a unique filename for the keep file prevents conflicts during resource merging in multi-module projects. (Android Developers)

In our case, the fix looked like this:

<!-- Path: app/src/main/res/raw/com.example.app.keep.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@style/Theme.StartingScreen_*,@style/AppTheme_*,@raw/fts" />

This approach makes your intent explicit: the @raw/fts resource must not be removed, even if it is only accessible via dynamic name resolution. This is critical for any resources read via Resources.getIdentifier(). (Android Developers)

Best Practices for AGP 9 and R8

First, for resources accessed dynamically, it is best to list them explicitly in tools:keep rather than relying on broad wildcards like @raw/*. The more specific your rules, the less likely you are to keep unnecessary resources, and the more predictable your build behavior becomes. (Android Developers)

Second, keep the shrinkMode in mind. By default, safe mode is used, which attempts to preserve resources that might be used dynamically via getIdentifier(). If you switch to tools:shrinkMode="strict", only explicitly referenced resources are kept, making the accuracy of your keep rules absolutely critical. (Android Developers)

Third, don't confuse the cause with the symptom. The problem isn't that your project uses DFM; it's that some resources are only reachable via runtime lookups and remain invisible to static analysis. DFM simply makes this scenario more common and more apparent during optimization. (Android Developers)

Summary

Missing resources in AGP 9 are not the result of magic or random error. They are a logical consequence of stricter static analysis in optimized resource shrinking. If your architecture relies on dynamic resource access, you must explicitly protect those resources using keep files and verify the behavior against your chosen shrinker mode. If crashes appear specifically after updating to AGP 8.9–9.0, double-check your plugin version, as known regressions in this area have already been addressed in subsequent patches. (Android Developers)

Useful Resources