<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Khuthadzo Gundula on Medium]]></title>
        <description><![CDATA[Stories by Khuthadzo Gundula on Medium]]></description>
        <link>https://medium.com/@kgundula?source=rss-1cf448bfcc79------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*gQ6-wRJQxXXzAzbu.png</url>
            <title>Stories by Khuthadzo Gundula on Medium</title>
            <link>https://medium.com/@kgundula?source=rss-1cf448bfcc79------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 04:31:04 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@kgundula/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[How We Shrank Our App’s DEX by 73%: Migrating to AGP 9 and Taming R8 Full Mode]]></title>
            <link>https://medium.com/@kgundula/how-we-shrank-our-apps-dex-by-73-migrating-to-agp-9-and-taming-r8-full-mode-b78a008882d3?source=rss-1cf448bfcc79------2</link>
            <guid isPermaLink="false">https://medium.com/p/b78a008882d3</guid>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[gradle]]></category>
            <category><![CDATA[performance-optimization]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[r8]]></category>
            <dc:creator><![CDATA[Khuthadzo Gundula]]></dc:creator>
            <pubDate>Fri, 09 Oct 2026 06:39:11 GMT</pubDate>
            <atom:updated>2026-10-09T06:39:11.495Z</atom:updated>
            <content:encoded><![CDATA[<p><em>From 18 Multidex shards to 3: auditing 1.5 million locked items, neutralising toxic vendor rules, and engineering precise ProGuard configurations.</em></p><p>Every mature Android codebase eventually develops a silent, creeping tax: ProGuard bloat. Over years of development, as features are added, security frameworks integrated, and new third-party SDKs pulled in, mysterious runtime crashes occur. Someone pushes a fix:</p><pre># &quot;Fixes ClassNotFoundException in release build&quot;<br>-keep class com.thirdparty.sdk.** { *; }  </pre><p>It works. The release build passes, the sprint finishes, and everyone moves on. Fast forward a few years. Your multi-module enterprise app takes 10 minutes to build, your APK download size creeps past 60 MB, and your cold-start launch time begins to climb. What happened?<br>Behind the scenes, those defensive blanket keep rules have essentially turned off your app optimiser.</p><p>Recently, our engineering team embarked on a mission to modernise our build infrastructure: upgrading to Android Gradle Plugin (AGP) 9 and undertaking a data-driven optimisation overhaul using <strong>R8 Full Mode</strong>.</p><p>When we ran our initial baseline audit, the results were a wake-up call: <strong>our app’s R8 optimisation score</strong> was a meagre 19.70%. More than <strong>1.51 million methods, fields, and classes</strong> were completely frozen — immune to shrinking, inlining, class merging, or obfuscation.</p><p>Here is the story of how we fixed it: migrating to AGP 9, discovering the hidden gems in library-bundled rules, turning blunt wildcards into precision surgical rules, and pushing our optimisation score to <strong>81.26%</strong> (and shrinking it to <strong>81.64%</strong>) while unlocking <strong>1.4 million code items</strong>.</p><p><strong>Key Takeaways</strong></p><ol><li>The Problem: Upstream vendor AARs and transitive R classes silently locked 80.3% of our enterprise codebase from R8 optimisation, freezing 1.51M methods and fields and bloating our release APK across 18 Multidex shards.</li><li>The Migration: Upgraded to AGP 9.4.0 and Gradle 9.1.0, adopted built-in Kotlin (2.2.10) and modern Kotlin DSL (.gradle.kts), eliminated legacy multidex libraries, and enforced non-transitive R classes (android.nonTransitiveRClass=true).</li><li>The R8 Breakthrough: Leveraged AGP’s modern optimization.keepRules.ignoreFrom DSL to filter toxic upstream wildcard rules, swapped blanket rules for precision <a href="http://twitter.com/Keep">@Keep</a> and member-level targets, and audited blast radius with R8’s Quantitative Configuration Analyser.</li><li>The Result: Uncompressed DEX shrank from 93.7 MB to 24.7 MB (-73.6%), locked code items plummeted from 1.51M to 97k (-93.6%), and 18 Multidex shards collapsed down to just 3 DEX files.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R9Y_yIMYbmEiTiueZqpYUQ.jpeg" /></figure><ol><li><strong>Setting the Stage: Upgrading to AGP 9 &amp; Built-in Kotlin (Kotlin DSL).</strong></li></ol><p>Before you can optimise your code with modern R8, your build system needs to be running on modern foundations. AGP 9 represents one of the most significant architectural leaps in the Android tooling ecosystem in recent years.</p><p><strong>Saying Goodbye to the External Kotlin Plugin</strong></p><p>In previous versions of AGP, every module in a multi-module project had to declare:</p><pre>// Kotlin DSL (build.gradle.kts)<br>plugins {<br>    id(&quot;com.android.library&quot;)<br>    id(&quot;org.jetbrains.kotlin.android&quot;) // Obsolete in AGP 9!<br>}</pre><p>With AGP 9, Kotlin compilation is built directly into the Android Gradle Plugin. AGP 9.4.0 natively compiles Kotlin 2.2.10 out of the box. Removing the redundant external Kotlin plugin across our modules simplified our Gradle graph and unified compiler configuration under AGP’s modern built-in Kotlin DSL:</p><p><em>(Note: Built-in Kotlin in AGP 9 applies directly to pure Android application and library modules (com.android.application and com.android.library). Multiplatform setups (Kotlin Multiplatform / KMP) sharing cross-platform logic continue to use JetBrains’ org.jetbrains.kotlin.multiplatform plugin.)</em></p><pre>// build.gradle.kts (or build.gradle)<br>kotlin {<br>    compilerOptions {<br>        freeCompilerArgs.addAll(<br>            &quot;-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi&quot;<br>        )<br>    }<br>}</pre><p><em>(Note: Because built-in Kotlin targets modern Kotlin 2.x, default interface methods (-Xjvm-default=all) are enabled by default, and kotlin.RequiresOptIn is stable — allowing you to purge obsolete compiler flags.)</em></p><p><strong>Modernising Annotation Processing: KSP and Legacy Kapt</strong></p><p>While migrating from kapt to KSP (Kotlin Symbol Processing) is the recommended path for modern Android development, some enterprise codebases still rely on legacy annotation processors that do not yet support KSP. For these scenarios, AGP 9 provides a dedicated com.android.legacy-kapt plugin that bridges legacy processors with built-in Kotlin:</p><pre>// Modern Kotlin DSL (root build.gradle.kts)<br>buildscript {<br>    dependencies {<br>        classpath(&quot;com.android.legacy-kapt:com.android.legacy-kapt.gradle.plugin:9.4.0&quot;)<br>    }<br>}<br><br>// Modern Kotlin DSL (app/build.gradle.kts)<br>plugins {<br>    id(&quot;com.android.application&quot;)<br>    id(&quot;com.android.legacy-kapt&quot;)<br>}    </pre><p><strong>Non-Transitive R Classes: Trimming the Resource Fat</strong></p><p>While non-transitive R classes were introduced in AGP 7.0 and became the default in AGP 8.0, migrating legacy enterprise apps that previously opted out android.nonTransitiveRClass=false is essential to unlocking maximum build speed and R8 efficiency.</p><p>In legacy Android builds, each subproject module inherited the resource IDs of every dependency it touched. In a large app, this meant thousands of duplicate resource fields generated across dozens of intermediate R.class files.</p><p>Ensuring non-transitive R classes are active in gradle.properties:</p><pre><br>android.nonTransitiveRClass=true</pre><p>guarantees each module’s R class contains only the resources declared within that specific module. External resources must be referenced via their explicit package name (e.g., com.example.designsystem.R.color.primary).</p><p>This change prevents massive resource ID duplication across intermediate modules, dramatically accelerating incremental compilation and DEX packaging.</p><p><strong>2. The Silent Optimisation Killer: R8 compatibility vs. Full Mode.</strong></p><p>Most developers know that R8 replaced ProGuard as Android’s code shrinker. What many don’t realise is that for years, AGP operated in R8 Compatibility Mode by default.</p><p><strong>What is R8 Full Mode?</strong></p><ul><li><strong>Compatibility Mode</strong>: Emulates legacy ProGuard behaviour. It makes conservative assumptions about reflection. If a class member is accessed or matches a rule, R8 often assumes the entire class might be accessed reflectively, preserving surrounding fields and methods.</li><li><strong>Full Mode</strong>: Strips unreferenced classes and interfaces, aggressively inlines single-use methods, merges interfaces with single implementations, and rewrites class structures.</li></ul><p>In modern AGP, Full Mode is the default (unless disabled with android.enableR8.fullMode=false). When we enabled Full Mode, we ran R8’s Configuration Analyser using the Gradle flag:</p><pre>./gradlew assembleRelease \<br>  -Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis</pre><p>This dumps an empirical protocol buffer graph detailing every keep rule, where it came from, and how many classes, fields, and methods it prevents R8 from optimising (its <strong>blast radius</strong>).</p><p><em>(Along with the raw .pb protobuf dump, R8 automatically generates an interactive configanalyzer.html visualiser dashboard right inside your output directory, calculating overall optimization, shrinking, and obfuscation scores and highlighting top offending rules without requiring any custom parsing tools.)</em><br>The result was astonishing:</p><pre>Baseline Analysis:<br>--------------------------------------------<br>Optimization Score : 19.70%<br>Shrinking Score    : 19.80%<br>Obfuscation Score  : 19.75%<br>Total Kept Items   : 1,516,929</pre><p>Over<strong> 80% of our app was locked</strong> from optimisation.</p><p><strong>The proof is in the APK: benchmarking compatibility mode vs. full mode &amp; non-transitive R</strong></p><p>To understand the real-world binary consequences of these configuration flags, we built and decompiled two production release APKs under identical release configurations (minifyEnabled true, shrinkResources true), isolating only the compiler and resource settings:</p><pre>#Modern Full Mode Build (release-with-r8) <br>android.enableR8.fullMode=true<br>android.nonTransitiveRClass=true<br>android.newDsl=false<br>android.uniquePackageNames=false</pre><p>When we inspected the Dalvik executable headers across both APKs, the empirical difference was staggering:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zgGBDmw1pFQQSgeybOogzA.png" /></figure><p>Why Did Compatibility Mode and Transitive R Explode So Badly?</p><ul><li>The 971,000 Field Explosion (android.nonTransitiveRClass=false): In a multi-module architecture, transitive R class generation forces every downstream module to duplicate every resource ID from every dependency it touches. Combined with android.nonFinalResIds=false and vendor blanket keep rules (-keep class **.R$*), R8 was prohibited from inlining integer resource constants into call sites. This single issue caused over 1.1 million static fields to be locked into the APK! Enabling android.nonTransitiveRClass=true dissolved 971,962 redundant fields instantly (an 87.6% drop).</li><li>Aggressive whole-app Optimisation in Full Mode (android.enableR8.fullMode=true): Compatibility Mode operates under conservative legacy ProGuard rules: if any class member is referenced or matched by a keep rule, R8 conservatively keeps the enclosing class structure and suppresses method inlining. Under Full Mode, R8 performs whole-app optimisation — merging single-implementation interfaces, inlining single-caller methods and constructors, and stripping unreferenced synthetic lambda classes. This eliminated 50,835 dead classes and 249,584 dead methods.</li><li>Multidex Collapse (18 -&gt; 3 DEX files) and Baseline Profile Optimisation: The Android DEX format imposes a strict 64K method/field reference limit per DEX file. Because compatibility mode + transitive R generated 1.1 million fields and ~395,000 methods, the compiler was forced to shard the application into 18 separate DEX files. During cold start, the Android Runtime (ART) must open, parse, verify, and memory-map each DEX shard. Collapsing 18 DEX files down to just 3 DEX files directly slashes classloader lookup contention and cold-start I/O overhead. Furthermore, by bringing the entire codebase down to fit within 3 shards, Android’s Baseline Profile compilation (dex-layout-optimizations) can group all critical launch classes into the primary classes.dex, ensuring peak startup performance without page-fault thrashing.</li></ul><p><strong>3. The Unsung Heroes: Benefiting from rules that libraries already shipped.</strong></p><p>When looking through our application’s proguard-rules.pro, we noticed massive chunks of rules dedicated to third-party SDKs — FIDO biometrics, networking libraries, JSON parsers, and UI frameworks.</p><pre># Found in app/proguard-rules.pro:<br>-keep class com.security.fido.** { *; }<br>-keep interface com.security.fido.** { *; }<br>-keep class io.kotzilla.** { *; }<br>-keep class com.squareup.moshi.** { *; }<br>-keep class androidx.compose.** { *; }</pre><p>Here was our first major breakthrough: Modern Android libraries already ship their own consumer ProGuard rules!</p><p>When an AAR dependency packages a proguard.txt or consumer-rules.pro, the Android Gradle Plugin automatically extracts and applies those rules during release compilation.</p><p>Because the library maintainers know their own internal reflection and JNI entry points far better than we do, their bundled rules are already calibrated to protect:</p><ul><li>Native JNI C++ methods (native &lt;methods&gt;;)</li><li>JNI callbacks and reflection classes</li><li>Internal serialisation interfaces</li></ul><p>By writing blanket rules like -keep class com.security.fido.** { *; } in our application file, we were overriding the library author’s careful work and forcing R8 to keep thousands of internal helper classes, private methods, and unused features that the library authors never intended to preserve!</p><p><strong>The Action:</strong><br>We deleted dozens of redundant, defensive library keep rules from app/proguard-rules.pro. The result? <strong>Tens of thousands of unused classes from our dependencies were immediately dropped by R8</strong>, with zero runtime issues because the libraries’ own bundled rules kept what was actually necessary.</p><p><strong>4. The Upstream Nightmare &amp; AGP’s Filtering Superpower: ignoreFrom</strong></p><p>Removing our own defensive rules was easy. But then we inspected the largest blast radius culprits in our R8 dump and discovered an alarming truth:</p><p><strong>The worst keep rules weren’t in our codebase at all</strong>. They were inside third-party vendor AARs.</p><p>One upstream vendor SDK bundled this single rule in its proguard.txt:</p><pre># Inside vendor-sdk-1.0.aar:<br>-keep class **.R$* { &lt;fields&gt;; }    # Locked 812,831 items!<br>-keep class androidx.** { *; }       # Locked 235,231 items!</pre><p>That one rule alone locked over <strong>one million code items</strong> across our entire application and disabled optimisation across all AndroidX libraries!<br>Historically, if a vendor packaged a terrible keep rule, Android developers had only two unpleasant options:</p><ul><li>Beg the vendor for an update and wait months.</li><li>Write a custom Gradle transform script to unpack the AAR, delete proguard.txt, and repackage it.</li></ul><p><strong>Enter Modern AGP: ignoreFrom</strong></p><p>While AGP 7.3 originally introduced ignoreExternalDependencies, modern AGP (AGP 8.4+ and AGP 9) refines and standardises this with the optimization.keepRules.ignoreFrom DSL (along with ignoreFromAllExternalDependencies = true).</p><p><em>(Note: </em><em>ignoreFrom is currently an incubating AGP DSL feature, giving developers surgical control over upstream AAR consumer rules.)</em></p><p>It allows you to selectively tell R8 to ignore the bundled consumer-rules.pro from specific Maven coordinates during the merge phase:</p><pre>// Modern Kotlin DSL (app/build.gradle.kts)<br>android {<br>    buildTypes {<br>        getByName(&quot;release&quot;) {<br>            optimization.keepRules {<br>                // Strip the toxic blanket rules from the vendor AAR:<br>                ignoreFrom(&quot;com.vendor.enterprise:problematic-sdk&quot;)<br>                ignoreFrom(&quot;org.conscrypt:conscrypt-android&quot;)<br>            }<br>        }<br>    }<br>}</pre><p>By filtering out the vendor’s nuclear keep rule, we immediately regained control.</p><p><strong><em>Pro Tip</em></strong><em>: You can inspect the merged output of all consumer rules before R8 runs by checking build/intermediates/proguard-files/ (or configuration.txt in your mapping outputs) to see exactly which vendor injected the offending keep rules.</em></p><p>Then, in our app/proguard-rules.pro, we added surgical replacement rules for only the exact runtime reflection classes that the vendor SDK actually needed:</p><pre># Surgical replacement: Keep only specific interfaces and caches<br>-keep @com.vendor.annotations.DoNotObfuscate class * {<br>     public &lt;methods&gt;;<br>     public &lt;fields&gt;;<br>}<br>-keep class com.vendor.modules.RendererCache {<br>     public &lt;methods&gt;;<br>     public &lt;fields&gt;;<br>}</pre><p>Score Impact: Our optimisation score jumped from 20.74% to 54.11%, freeing nearly 600,000 code items in one stroke.</p><p><strong>5. From Blanket to Scalpel: Surgical Keep Rule Patterns</strong></p><p>Once the major external offenders were neutralized, we turned our attention to common architectural patterns that inadvertently freeze thousands of code items.</p><p><strong>Pattern 1: Moshi JSON Serialization</strong></p><p>In many Android apps, you’ll find a rule like this:</p><pre># The &quot;Safe&quot; Moshi Rule<br>-keepclassmembers @com.squareup.moshi.JsonClass class * {<br>    &lt;init&gt;(...);<br>    &lt;fields&gt;;<br>}</pre><p>At first glance, this seems reasonable: if a class is annotated with <a href="http://twitter.com/JsonClass">@JsonClass</a>, keep its constructor and its fields.</p><p><strong>The Problem</strong>: &lt;fields&gt;; tells R8 to preserve <strong>every single private field</strong> across all your data classes. But in modern Android apps, models use Moshi’s annotation processor or KSP (<a href="http://twitter.com/JsonClass">@JsonClass</a>(generateAdapter = true)).</p><p>When adapters are generated at compile time, Moshi bypasses reflective field access entirely: the generated adapter invokes public accessors or direct constructors. Keeping &lt;fields&gt;; was freezing every backing property for no reason.</p><p>All R8 needs is the un-obfuscated model class name (so Moshi can locate ${ClassName}JsonAdapter via reflection) and the target constructor.<br>We refined the rule to keep class names (so Moshi can locate the generated *JsonAdapter reflectively) and constructors invoked by the adapter:</p><pre>-keepnames @com.squareup.moshi.JsonClass class *<br>-keepclassmembers @com.squareup.moshi.JsonClass class * {<br>    &lt;init&gt;(...);<br>}</pre><p><em>(If your codebase still maintains models with reflective serialization using </em><a href="http://twitter.com/Json"><em>@Json</em></a><em>, ensure the annotation explicitly targets the field via </em><a href="http://twitter.com/field"><em>@field</em></a><em>:Json or preserves public property accessors.)</em></p><p><strong>Result</strong>: Over 9,400 private fields were instantly unlocked, allowing R8 to inline, rename, or eliminate them when unused.</p><p><strong>Pattern 2: Security Providers &amp; Native JNI (Conscrypt)</strong></p><p>Conscrypt provides high-performance TLS/SSL encryption via native BoringSSL C++ code. Its bundled rule was a blunt package-wide keep:</p><pre>-keep class org.conscrypt.** { *; }</pre><p>This froze 4,910 classes, fields, and methods from optimization.</p><p>Because native C++ libraries interact with Java through the Java Native Interface (JNI), you must protect two things: the Java-to-C native method declarations and any Java callback classes/methods that C++ invokes via env-&gt;GetMethodID(…) or env-&gt;GetFieldID(…) — along with the Security.addProvider() entry point.</p><p>We ignored Conscrypt’s external rules and provided surgical JNI targets:</p><pre># Precision Conscrypt Rule<br>-keep class org.conscrypt.OpenSSLProvider { public &lt;init&gt;(...); }<br>-keepclassmembers class org.conscrypt.** {<br>    native &lt;methods&gt;;<br>}<br># Preserve Java entry points and callbacks invoked by native BoringSSL code:<br>-keep class org.conscrypt.NativeCrypto { *; }</pre><p>R8 was now free to optimise all internal Java plumbing, while native cryptographic calls continued to link flawlessly.</p><p><strong>The Multiplier Effect: Collapsing the Transitive Blast Radius</strong></p><p>When we neutralised the broad Moshi rules, Conscrypt’s package wildcard, and eliminated dead subsumed rules in our own configuration, the impact went far beyond those immediate classes. In R8, blanket rules create a cascading <strong>blast radius</strong> — forcing R8 to conservatively retain hundreds of transitive utility classes, companion objects, and interfaces that touch those hierarchies. Removing these roots collapsed the entire blast radius, unlocking over <strong>400,000 dependent items</strong> in this final optimisation pass.</p><p><strong>Pattern 3: Navigation SafeArgs &amp; Code Annotations</strong></p><p>Instead of keeping string patterns in ProGuard:</p><pre>-keep class com.mycompany.app.presentation.common.error.ButtonAction { *; }<br>-keep class com.mycompany.app.presentation.common.error.ErrorType { *; }</pre><p>We applied <a href="http://twitter.com/androidx">@androidx</a>.annotation.Keep directly on the Kotlin enum declarations in code:</p><pre>@Keep<br>enum class ButtonAction { RETRY, DISMISS, SUBMIT}</pre><p>Keeping the rule in Kotlin source code colocates the optimisation requirement with the code itself, ensuring that if the class is ever deleted or refactored, the keep directive cleanly disappears with it.</p><p><strong>The Keep Rule “Smell Test” Cheat Sheet</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6_szpjRwzNNMesCv2dsAMw.png" /><figcaption>Smell Test Cheat Sheet</figcaption></figure><p><strong>6. The Results: By the Numbers</strong></p><p>Through four structured iterations of auditing, filtering, and refining, here is how the metrics evolved:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t5nXi-wKDUCMPpuoJsa-5g.png" /><figcaption>R8 optimisation metrics</figcaption></figure><p>Milestone Evolution (At a Glance):</p><ul><li><strong>Baseline</strong>: 19.70% Optimisation | 1,516,929 items locked (80%+ frozen)</li><li><strong>Round 1 (Dead App Rules)</strong>: 20.74% Optimisation | Purged subsumed app rules</li><li><strong>Round 2 (Core Banking SDK)</strong>: 54.11% Optimisation | Neutralised blanket vendor rules via ignoreFrom (+590,000 items freed)</li><li><strong>Round 3 (Features &amp; Security SDK)</strong>: 78.97% Optimisation | Isolated internal features &amp; security wildcards (+326,000 items freed)</li><li><strong>Round 4 (Moshi &amp; Conscrypt JNI)</strong>: 81.26% optimisation / 81.64% shrinking | Precision JNI &amp; Moshi codegen rules (+426,000 items freed)</li><li><strong>Total Net Gain</strong>: 97,576 kept items remaining (-93.6% / 1.42M items unlocked)</li></ul><p><strong>7. The 100% Optimisation Myth: Why you can’t (and shouldn’t) reach 100%</strong></p><p>A common question engineering leaders ask when looking at optimisation dashboards is, <em>&quot;Why are we at 81%? Can’t we get to 100%?”</em></p><p>The short answer: <strong>If your native Android app reached 100% optimization, it would crash instantly on launch.</strong></p><p>In native Android, approximately 15% to 20% of code must be kept intact by architectural necessity:</p><ul><li><strong>OS Manifest Entry Points</strong>: Every activity, service, receiver, and contentProvider declared in AndroidManifest.xml must be located and instantiated reflectively by the Android Framework (ActivityThread via the application ClassLoader) using its exact class name.</li><li><strong>Native C/C++ JNI Tables</strong>: Libraries with .so binaries (biometrics, fraud detection, BoringSSL) look up Java methods via C symbol tables. If renamed or stripped, the app throws an immediate UnsatisfiedLinkError.</li><li><strong>Serialization Contracts</strong>: Network payloads exchanged with backend APIs require schema property names to remain consistent.</li><li><strong>Jetpack Framework Metadata</strong>: Jetpack Compose runtime group mappings and dependency injection (Koin) reflection require metadata preservation.</li></ul><p>An optimisation score between 80% and 85% is the practical gold standard for a large enterprise Android app.</p><p><strong>8. Five Rules of Thumb for Android Keep Rules in 2026</strong></p><p>To keep your Android app lean and responsive, establish these practices across your engineering team:</p><ol><li><strong>Audit Upstream Rules First</strong>: When importing a new SDK, inspect its AAR’s proguard.txt. If you see -keep class **.R$* or -keep class com.package.** { *; }, don’t accept it. Use AGP’s optimization.keepRules.ignoreFrom and write surgical replacement rules.</li><li><strong>Trust Good Libraries</strong>: Don’t write defensive “keep” rules for well-maintained libraries that already bundle their consumer rules. Let the library author’s rules do their job.</li><li><strong>Never Use Package-Wide Wildcards</strong>: Replace -keep class com.foo.** { *; } with targeted entry points, interfaces, or <a href="http://twitter.com/Keep">@Keep</a> annotations.</li><li><strong>Distinguish Class vs. Member Keeps</strong>: If a class only needs reflection on its constructor, target it specifically: -keepclassmembers class com.example.MyClass { &lt;init&gt;(…); } or -keepclassmembers <a href="http://twitter.com/MyAnnotation">@MyAnnotation</a> class * { &lt;init&gt;(…); }. Never write a catch-allclass * member keeps or retain &lt;fields&gt; ; unless reflective serialisation strictly requires it.</li><li><strong>Measure Quantitatively</strong>: Don’t guess what your rules are doing. Run R8’s configuration analyser (-Dcom.android.tools.r8.dumpkeepradiustodirectory), inspect the blast radius, and make decisions based on real data. (<em>Prerequisites: R8 9.3.7-dev+ / AGP 9.3+. The interactive configanalyzer.html opens directly in your browser, while raw protobuf data can be parsed via Python if running custom CI checks.</em>)</li></ol><p><strong>Final Thoughts</strong></p><p>Migrating to AGP 9 and optimising R8 is not just about improving the numbers on a chart. By reclaiming over 1.4 million code items from the frozen wasteland of blanket keep rules, we significantly trimmed our app’s binary footprint, improved method inlining and dead-code stripping, and established a faster, more resilient build pipeline. Take a look at your app’s proguard-rules.pro today. You might just find a million locked items waiting to be set free.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b78a008882d3" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>