<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Uray Febri</title>
    <description>The latest articles on DEV Community by Uray Febri (@ufebri).</description>
    <link>https://dev.to/ufebri</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4132286%2F6cfa8da7-d472-4ba2-87c0-3212f8f929be.png</url>
      <title>DEV Community: Uray Febri</title>
      <link>https://dev.to/ufebri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ufebri"/>
    <language>en</language>
    <item>
      <title>Debugging R8 Release-Only Crashes Without Disabling Optimization</title>
      <dc:creator>Uray Febri</dc:creator>
      <pubDate>Sat, 19 Sep 2026 03:07:27 +0000</pubDate>
      <link>https://dev.to/ufebri/debugging-r8-release-only-crashes-without-disabling-optimization-49pd</link>
      <guid>https://dev.to/ufebri/debugging-r8-release-only-crashes-without-disabling-optimization-49pd</guid>
      <description>&lt;p&gt;A debug build works. The release build installs. Then one screen crashes only after minification is enabled.&lt;/p&gt;

&lt;p&gt;The tempting fix is immediate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-keep class com.example.** { *; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or worse, turn R8 off and ship.&lt;/p&gt;

&lt;p&gt;Both can make the symptom disappear. Neither tells you what broke.&lt;/p&gt;

&lt;p&gt;A release-only R8 failure is usually easier to solve when you treat it as an &lt;strong&gt;indirect-reference debugging problem&lt;/strong&gt;, not a ProGuard guessing contest. The goal is to identify the runtime contract that static analysis cannot see, preserve exactly that contract, and keep the rest of the optimizer working.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Prove That Optimization Is the Boundary
&lt;/h2&gt;

&lt;p&gt;Do not begin by editing keep rules.&lt;/p&gt;

&lt;p&gt;Reproduce the failure with the same release variant that users receive. That matters because build type, flavor, dependency graph, resource shrinking, signing configuration, and generated code can all differ from debug.&lt;/p&gt;

&lt;p&gt;A useful first comparison is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Debug build        → works
Release, R8 on     → fails
Release, R8 off    → works
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That strongly points toward optimization, shrinking, obfuscation, or a rule consumed from a dependency. It still does not prove which class needs to be kept.&lt;/p&gt;

&lt;p&gt;If your release pipeline is already automated, this is also why &lt;a href="https://raylabs.app/articles/automating-android-ci-workflows-with-github-actions-and-grad/" rel="noopener noreferrer"&gt;Android CI should build the real release variants&lt;/a&gt;, not only compile debug APKs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrace Before You Read the Crash
&lt;/h2&gt;

&lt;p&gt;An obfuscated stack trace is evidence with its labels removed.&lt;/p&gt;

&lt;p&gt;R8 produces &lt;code&gt;mapping.txt&lt;/code&gt; for the optimized variant. Preserve the mapping file for every published build, because a later build can overwrite the local copy. Android's documentation recommends using &lt;code&gt;retrace&lt;/code&gt; when the original stack trace is not automatically deobfuscated.&lt;/p&gt;

&lt;p&gt;A manual workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ANDROID_HOME&lt;/span&gt;/cmdline-tools/latest/bin/retrace &lt;span class="se"&gt;\&lt;/span&gt;
  app/build/outputs/mapping/release/mapping.txt &lt;span class="se"&gt;\&lt;/span&gt;
  trace.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important question after retracing is not simply, "Which class crashed?"&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What runtime mechanism expected this class, method, field, constructor, annotation, or name to remain discoverable?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question usually gets you much closer to the missing rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look for Indirect References
&lt;/h2&gt;

&lt;p&gt;R8 is very good at following static references. Problems appear when the program reaches code in ways that static analysis cannot fully infer.&lt;/p&gt;

&lt;p&gt;Common boundaries include reflection, classes loaded from string names, JNI, serialization frameworks that inspect members, and libraries that discover implementations dynamically.&lt;/p&gt;

&lt;p&gt;Android's R8 guidance specifically calls out reflection-related failures such as &lt;code&gt;ClassNotFoundException&lt;/code&gt;, &lt;code&gt;NoSuchMethodException&lt;/code&gt;, &lt;code&gt;NoSuchFieldException&lt;/code&gt;, &lt;code&gt;NoClassDefFoundError&lt;/code&gt;, &lt;code&gt;NoSuchMethodError&lt;/code&gt;, and &lt;code&gt;NoSuchFieldError&lt;/code&gt; as useful signals.&lt;/p&gt;

&lt;p&gt;Suppose an integration does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clazz&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;className&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clazz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDeclaredConstructor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;newInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To the runtime, that class is required.&lt;/p&gt;

&lt;p&gt;To a static optimizer, a string containing a class name is not necessarily a normal code reference. If nothing else reaches the class, removing or renaming it may be perfectly logical from R8's perspective.&lt;/p&gt;

&lt;p&gt;The bug is therefore not "R8 randomly deleted my code." The real issue is that a runtime dependency was invisible to static analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the Rules R8 Actually Received
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;proguard-rules.pro&lt;/code&gt; is not the whole configuration.&lt;/p&gt;

&lt;p&gt;Rules can come from the application, Android tooling, and consumer rules packaged by dependencies. Android's troubleshooting documentation points to the merged R8 configuration under the build outputs, and the Android Developers Blog also recommends printing the final configuration when you need to understand where a rule came from.&lt;/p&gt;

&lt;p&gt;That changes the investigation from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I think this is my R8 configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the configuration R8 actually evaluated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is especially useful when optimization is weaker than expected. A dependency can bring broad rules or global options that keep far more code than you intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use &lt;code&gt;-whyareyoukeeping&lt;/code&gt; in the Opposite Case
&lt;/h2&gt;

&lt;p&gt;Not every R8 problem is missing code.&lt;/p&gt;

&lt;p&gt;Sometimes the APK remains unexpectedly large because a class you expected R8 to remove is still present. The &lt;code&gt;-whyareyoukeeping&lt;/code&gt; diagnostic rule asks R8 to show the reference chain responsible for retaining that code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-whyareyoukeeping class com.example.feature.LegacyEntryPoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it as a debugging instrument, not permanent configuration. Android explicitly cautions against checking it into the codebase because it can slow builds.&lt;/p&gt;

&lt;p&gt;This gives you two complementary investigations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runtime crash
    ↓
What required code did R8 fail to see?

Unexpected retained code
    ↓
Why does R8 believe this code is reachable?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are better than adding wildcards until the build behaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the Narrowest Rule That Expresses the Runtime Contract
&lt;/h2&gt;

&lt;p&gt;A broad rule is useful as a temporary experiment.&lt;/p&gt;

&lt;p&gt;If keeping an entire package makes the crash disappear, you have learned that the missing contract probably lives inside that package. That is diagnostic progress, but it should not automatically become the final fix.&lt;/p&gt;

&lt;p&gt;Android recommends keep rules that are as specific as possible and warns against long-lived package-wide rules such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-keep class com.example.feature.** { *; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, describe the actual boundary.&lt;/p&gt;

&lt;p&gt;If only implementations of an interface are dynamically loaded, preserve those implementations and only the constructor the loader needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-keep class * implements com.example.runtime.StartupTask {
    &amp;lt;init&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If reflection requires a particular member, target the member rather than freezing the entire class.&lt;/p&gt;

&lt;p&gt;The best keep rule is not the shortest rule. It is the rule whose scope matches the runtime behavior you can explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer Annotations When the Contract Belongs to Your Code
&lt;/h2&gt;

&lt;p&gt;When your own architecture intentionally exposes code to reflection or discovery, annotations can make the boundary explicit.&lt;/p&gt;

&lt;p&gt;Instead of maintaining a fragile list of class names, mark the code that participates in the runtime contract and write a rule against that annotation. Android's keep-rule guidance recommends this pattern because the relationship between source code and preservation becomes visible.&lt;/p&gt;

&lt;p&gt;That is a maintainability improvement as much as an optimization improvement.&lt;/p&gt;

&lt;p&gt;A future developer can answer "Why is this kept?" from the source instead of archaeology in a large rules file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Not Test the Fix Only by Launching the App
&lt;/h2&gt;

&lt;p&gt;A release build that reaches the home screen proves very little.&lt;/p&gt;

&lt;p&gt;The verification should exercise the behavior that crossed the invisible boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deserialize the affected model;&lt;/li&gt;
&lt;li&gt;open the screen that instantiates the class dynamically;&lt;/li&gt;
&lt;li&gt;run the JNI call;&lt;/li&gt;
&lt;li&gt;execute the deep link or navigation route;&lt;/li&gt;
&lt;li&gt;test the flavor where the dependency is actually included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then inspect the optimized artifact again.&lt;/p&gt;

&lt;p&gt;If you fixed a crash with a narrower rule, verify both sides of the trade-off: the feature still works &lt;strong&gt;and&lt;/strong&gt; R8 is still allowed to optimize unrelated code.&lt;/p&gt;

&lt;p&gt;For multi-flavor apps, this becomes even more important because a rule that appears correct in one variant can hide a dependency-specific problem in another. The same principle applies when &lt;a href="https://raylabs.app/articles/analyzing-technical-review-feedback-for-multi-flavor-android/" rel="noopener noreferrer"&gt;reviewing technical feedback across Android flavors&lt;/a&gt;: verify the claim against the exact variant and dependency boundary involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Repeatable R8 Debugging Loop
&lt;/h2&gt;

&lt;p&gt;The process can be reduced to a small loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reproduce the release-only failure
              ↓
Retrace with the matching mapping file
              ↓
Identify the runtime boundary
              ↓
Inspect merged R8 configuration
              ↓
Test a temporary isolation rule
              ↓
Replace it with the narrowest valid rule
              ↓
Exercise the affected release behavior
              ↓
Verify optimization still works elsewhere
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key step is the one developers often skip: &lt;strong&gt;replace the temporary broad rule&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A wildcard can be a good diagnostic switch. It is rarely a good explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat Keep Rules as Architecture Documentation
&lt;/h2&gt;

&lt;p&gt;R8 failures feel mysterious when the rules file becomes a collection of copied incantations.&lt;/p&gt;

&lt;p&gt;They become much more predictable when every rule answers three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What code is reached indirectly?&lt;/li&gt;
&lt;li&gt;Which runtime mechanism reaches it?&lt;/li&gt;
&lt;li&gt;What is the smallest surface that mechanism requires?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you can answer those, the rule stops being a workaround. It becomes documentation for a boundary in your architecture.&lt;/p&gt;

&lt;p&gt;That is the standard I would use before shipping an R8 fix: not merely &lt;strong&gt;"the release build no longer crashes,"&lt;/strong&gt; but &lt;strong&gt;"we know why the optimizer could not see this dependency, and the rule preserves exactly what runtime needs."&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://raylabs.app/articles/debugging-r8-release-only-crashes/" rel="noopener noreferrer"&gt;RayLabs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>r8</category>
      <category>proguard</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Native Android OTA Updates: What You Can Patch Without Shipping a New APK</title>
      <dc:creator>Uray Febri</dc:creator>
      <pubDate>Sat, 19 Sep 2026 03:05:53 +0000</pubDate>
      <link>https://dev.to/ufebri/native-android-ota-updates-what-you-can-patch-without-shipping-a-new-apk-2hh5</link>
      <guid>https://dev.to/ufebri/native-android-ota-updates-what-you-can-patch-without-shipping-a-new-apk-2hh5</guid>
      <description>&lt;p&gt;Over-the-air updates sound like a simple product request: fix production without waiting for users to install another app version. On native Android, however, the phrase &lt;strong&gt;OTA update&lt;/strong&gt; can describe several very different mechanisms. Some are standard platform features. Some update configuration or content rather than executable code. Others cross directly into security and store-policy boundaries.&lt;/p&gt;

&lt;p&gt;That distinction matters because a native Kotlin application does not have a direct equivalent to every code-push system available in other mobile stacks. A production Android app can change a surprising amount of behavior without a new APK, but the safe architecture is usually &lt;strong&gt;not&lt;/strong&gt; to download replacement Kotlin or DEX code from your own server.&lt;/p&gt;

&lt;p&gt;The useful question is therefore not, “How do I add OTA to Android?” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What exactly needs to change after release, and which layer should own that change?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the requirement is classified correctly, the implementation options become much clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  OTA Is an Outcome, Not One Technology
&lt;/h2&gt;

&lt;p&gt;Teams often group four different update problems under the same label:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Better mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deliver a new signed Android binary&lt;/td&gt;
&lt;td&gt;Google Play release + In-App Updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change feature availability or thresholds&lt;/td&gt;
&lt;td&gt;Remote configuration / feature flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change copy, catalog data, forms, or server-owned content&lt;/td&gt;
&lt;td&gt;API-driven content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change layout within predefined components&lt;/td&gt;
&lt;td&gt;Server-driven UI with a constrained schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace arbitrary Kotlin/DEX/native code&lt;/td&gt;
&lt;td&gt;Usually requires a new app release&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This classification is more useful than starting with a code-push SDK. It keeps the update mechanism proportional to the thing that actually changes.&lt;/p&gt;

&lt;p&gt;If a production issue can be fixed by changing a timeout, disabling a feature, switching an endpoint, or hiding a problematic flow, shipping a complete binary is unnecessarily heavy. But if the fix changes an Activity, a native SDK, a Room migration, a permission declaration, or compiled Kotlin behavior, a normal application release is usually the correct boundary.&lt;/p&gt;

&lt;p&gt;That same risk-first thinking also applies to broader deployment work. A reliable mobile release is less about one clever update mechanism and more about &lt;a href="https://raylabs.app/articles/executing-a-smooth-technical-rollout/" rel="noopener noreferrer"&gt;executing a smooth technical rollout&lt;/a&gt; with explicit ownership, verification, and rollback paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Use Google Play In-App Updates for New Binaries
&lt;/h2&gt;

&lt;p&gt;For applications distributed through Google Play, the most straightforward native update path is the &lt;a href="https://developer.android.com/guide/playcore/in-app-updates/kotlin-java" rel="noopener noreferrer"&gt;Play In-App Updates API&lt;/a&gt;. Google provides the API for Kotlin and Java applications through the Play Core app-update library.&lt;/p&gt;

&lt;p&gt;The important detail is that &lt;strong&gt;In-App Updates does not bypass the store release process&lt;/strong&gt;. Your new version still goes through Google Play. The API improves the user experience of discovering and installing an available version from inside the application.&lt;/p&gt;

&lt;p&gt;Android supports two main user experiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flexible updates&lt;/strong&gt; download while the user continues using the app and can be completed later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immediate updates&lt;/strong&gt; present a blocking update flow when continuing on the old version is not acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes In-App Updates useful for compatibility breaks, security-sensitive releases, mandatory backend migrations, or important fixes where adoption speed matters. It is not a mechanism for changing compiled code independently of the store.&lt;/p&gt;

&lt;p&gt;A simple architecture might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App starts
   ↓
Check Play update availability
   ↓
Classify update policy
   ├── optional → flexible flow
   └── required → immediate flow
   ↓
Google Play delivers signed update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy decision should live outside the UI where possible. A ViewModel or use case can decide whether the current version is acceptable, while the Activity owns the Play update UI contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Move Emergency Knobs Into Remote Configuration
&lt;/h2&gt;

&lt;p&gt;Many requests for “native OTA” are actually requests for &lt;strong&gt;operational control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine a release where a new checkout path starts failing for a subset of users. If the app already has a remotely controlled feature flag, the team may be able to disable that path immediately while preparing a proper binary fix.&lt;/p&gt;

&lt;p&gt;Useful remotely controlled values include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feature enablement,&lt;/li&gt;
&lt;li&gt;rollout percentages,&lt;/li&gt;
&lt;li&gt;API timeout thresholds,&lt;/li&gt;
&lt;li&gt;minimum supported version,&lt;/li&gt;
&lt;li&gt;maintenance banners,&lt;/li&gt;
&lt;li&gt;endpoint selection from a pre-approved set,&lt;/li&gt;
&lt;li&gt;experiment variants,&lt;/li&gt;
&lt;li&gt;kill switches for risky functionality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical design rule is that remote configuration should select among &lt;strong&gt;capabilities already shipped in the binary&lt;/strong&gt;. It should not become an improvised programming language.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;RuntimePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newCheckoutEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;minimumSupportedVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;requestTimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server can change the values, but the application still defines what those values are allowed to do.&lt;/p&gt;

&lt;p&gt;This produces a powerful production property: some incidents can be mitigated in minutes without turning the app into a remote-code execution platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: Make Content Server-Driven
&lt;/h2&gt;

&lt;p&gt;Text, product catalogs, FAQs, onboarding messages, campaign cards, and many business rules are data. They do not need to be hard-coded into the APK.&lt;/p&gt;

&lt;p&gt;If the product changes these frequently, model them as server-owned content from the beginning.&lt;/p&gt;

&lt;p&gt;The boundary can be as simple as an API response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Scheduled maintenance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Some transfers may be delayed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native application owns rendering and allowed behaviors. The backend owns the current content.&lt;/p&gt;

&lt;p&gt;This is technically an OTA change from the user's perspective, but no executable code has changed. It is also easier to test because the client can validate the response against a stable contract.&lt;/p&gt;

&lt;p&gt;For resilient applications, cache the last valid payload, reject unsupported schema versions, define safe defaults, and make malformed remote content fail closed rather than crashing the screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 4: Use Constrained Server-Driven UI When Layout Must Change
&lt;/h2&gt;

&lt;p&gt;Server-driven UI extends the same idea from content to composition.&lt;/p&gt;

&lt;p&gt;Instead of the server sending arbitrary executable logic, it sends a declarative schema using components that already exist in the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"screen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"promotion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Weekend offer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product_grid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"featured"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open_checkout"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Android client decides which component types and actions are valid. Unknown components can be ignored or replaced with a safe fallback.&lt;/p&gt;

&lt;p&gt;This architecture can reduce release pressure for highly dynamic surfaces, but it comes with real costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;schema versioning,&lt;/li&gt;
&lt;li&gt;accessibility guarantees,&lt;/li&gt;
&lt;li&gt;analytics consistency,&lt;/li&gt;
&lt;li&gt;preview tooling,&lt;/li&gt;
&lt;li&gt;caching and offline behavior,&lt;/li&gt;
&lt;li&gt;backward compatibility,&lt;/li&gt;
&lt;li&gt;more complex testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Server-driven UI is therefore not “free OTA.” It exchanges binary-release frequency for platform complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Downloading New Kotlin or DEX Code Is a Different Category
&lt;/h2&gt;

&lt;p&gt;Native Android code eventually becomes executable artifacts such as DEX and native libraries. Loading replacement executable code from a remote source changes the security model significantly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.android.com/privacy-and-security/security-tips" rel="noopener noreferrer"&gt;Android's security guidance&lt;/a&gt; strongly discourages dynamically loading code from outside the application APK because it increases exposure to code injection and tampering and complicates verification and version management.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://support.google.com/googleplay/android-developer/answer/16559646?hl=en-GB" rel="noopener noreferrer"&gt;Google Play's Device and Network Abuse policy&lt;/a&gt; is even more important for Play-distributed applications. It states that an app distributed through Google Play may not update itself outside Google Play's update mechanism and may not download executable code such as DEX, JAR, or &lt;code&gt;.so&lt;/code&gt; files from another source. The policy documents an exception for code running in a virtual machine or interpreter under the described conditions, but that is not a blanket permission to build an unrestricted native code-push system.&lt;/p&gt;

&lt;p&gt;So a custom design like this deserves immediate scrutiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Native Android app
      ↓
download patch.dex from private server
      ↓
load classes dynamically
      ↓
replace production behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if it can be made to work technically, technical possibility is not the same as a sound distribution architecture.&lt;/p&gt;

&lt;p&gt;There are also platform-level hardening trends to consider. Android's security guidance continues to push developers away from writable dynamically loaded code, reinforcing the principle that executable updates should remain tightly controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Shorebird Feels Different
&lt;/h2&gt;

&lt;p&gt;Flutter developers have a more visible example of production code push in &lt;a href="https://docs.shorebird.dev/code-push/" rel="noopener noreferrer"&gt;Shorebird&lt;/a&gt;. Shorebird uses a modified Flutter engine that can download and apply patches to Dart code. Its documentation describes Android support and explains that patches can modify Dart application code while native Java/Kotlin changes, native dependencies, assets, and Flutter engine changes remain outside the patch boundary.&lt;/p&gt;

&lt;p&gt;That architecture is possible because Flutter already introduces a runtime and engine boundary between much of the application's Dart code and the native platform.&lt;/p&gt;

&lt;p&gt;A pure native Kotlin application does not have that same boundary by default.&lt;/p&gt;

&lt;p&gt;This is the key comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Native Android&lt;/th&gt;
&lt;th&gt;Flutter + Shorebird&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Update full application binary&lt;/td&gt;
&lt;td&gt;Play release&lt;/td&gt;
&lt;td&gt;Store release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt/install store update in app&lt;/td&gt;
&lt;td&gt;Play In-App Updates&lt;/td&gt;
&lt;td&gt;Store-specific flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change remote flags/content&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patch Dart application code&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;Supported by Shorebird&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patch Java/Kotlin native code&lt;/td&gt;
&lt;td&gt;New binary expected&lt;/td&gt;
&lt;td&gt;Not supported by Shorebird patching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patch native SDK or manifest&lt;/td&gt;
&lt;td&gt;New binary expected&lt;/td&gt;
&lt;td&gt;New binary expected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://docs.shorebird.dev/code-push/faq/" rel="noopener noreferrer"&gt;Shorebird's Code Push FAQ&lt;/a&gt; also documents the practical patch boundary and store-policy considerations that still apply to code-push workflows.&lt;/p&gt;

&lt;p&gt;So the useful lesson from Shorebird is not “native Android needs a Shorebird clone.” The lesson is to identify a &lt;strong&gt;stable runtime boundary&lt;/strong&gt; and be explicit about what can safely change inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Native Android Update Architecture
&lt;/h2&gt;

&lt;p&gt;For most production Kotlin applications, a layered approach is more robust than arbitrary code push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1: Store binary
Kotlin, Compose/XML, native SDKs, manifest, database migrations
                ↓
Layer 2: Runtime policy
Feature flags, kill switches, minimum version, rollout controls
                ↓
Layer 3: Remote content
Copy, catalogs, configuration, campaigns
                ↓
Layer 4: Constrained server-driven UI
Only when the product genuinely needs dynamic composition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer has a different release velocity and a different risk profile.&lt;/p&gt;

&lt;p&gt;This also makes CI/CD easier to reason about. Binary changes go through the strongest build and test gates. Configuration changes can have schema validation and staged rollout. Content changes can have editorial validation. Server-driven UI payloads can have contract tests and preview environments.&lt;/p&gt;

&lt;p&gt;The goal is not to make every layer deploy at the same speed. The goal is to make &lt;strong&gt;the safest layer capable of solving the problem deploy quickly enough&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for Rollback Before You Design for OTA
&lt;/h2&gt;

&lt;p&gt;Fast delivery without fast recovery is not a mature update system.&lt;/p&gt;

&lt;p&gt;Before adding any remote-control mechanism, define its rollback behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens if the configuration service is unavailable?&lt;/li&gt;
&lt;li&gt;Is the last known-good value cached?&lt;/li&gt;
&lt;li&gt;Can a feature be disabled independently?&lt;/li&gt;
&lt;li&gt;Can a malformed payload crash application startup?&lt;/li&gt;
&lt;li&gt;Does the server know which client schema versions it is targeting?&lt;/li&gt;
&lt;li&gt;Can an emergency binary release override remote state?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For configuration, prefer typed defaults and bounded values. For server-driven UI, version schemas and reject unsupported actions. For Play releases, use staged rollouts where appropriate and monitor the version-specific failure rate.&lt;/p&gt;

&lt;p&gt;The release mechanics should also be connected to your broader &lt;a href="https://raylabs.app/articles/memahami-manfaat-ci-cd-proyek-untuk-otomatisasi-perangkat-lu/" rel="noopener noreferrer"&gt;CI/CD feedback loop&lt;/a&gt; so a fast mitigation does not become an excuse to bypass tests for the permanent fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the Smallest Update Surface That Solves the Problem
&lt;/h2&gt;

&lt;p&gt;The strongest native Android OTA strategy is usually a collection of deliberately limited mechanisms rather than one universal patcher.&lt;/p&gt;

&lt;p&gt;Use Play In-App Updates when users need a new signed application version. Use feature flags and remote configuration for operational switches. Move genuinely dynamic content behind APIs. Consider constrained server-driven UI only when the product benefits enough to justify the schema and testing cost. Treat arbitrary executable-code downloading as a security and policy boundary, not as a shortcut around release engineering.&lt;/p&gt;

&lt;p&gt;That approach may sound less magical than “patch anything instantly,” but it produces a system that is easier to secure, test, roll back, and explain.&lt;/p&gt;

&lt;p&gt;For native Android, that is often the more useful definition of OTA: &lt;strong&gt;not eliminating releases, but reducing how often a production problem actually requires one.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://raylabs.app/articles/native-android-over-the-air-updates/" rel="noopener noreferrer"&gt;RayLabs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>mobile</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
