<?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[ProAndroidDev - Medium]]></title>
        <description><![CDATA[The latest posts from Android Professionals and Google Developer Experts. - Medium]]></description>
        <link>https://proandroiddev.com?source=rss----c72404660798---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>ProAndroidDev - Medium</title>
            <link>https://proandroiddev.com?source=rss----c72404660798---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 20:55:28 GMT</lastBuildDate>
        <atom:link href="https://proandroiddev.com/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[Your Android APK Is Bigger Than It Needs to Be: 22 Smart Ways to Reduce Its Size]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://proandroiddev.com/your-android-apk-is-bigger-than-it-needs-to-be-22-smart-ways-to-reduce-its-size-d39f678e5b43?source=rss----c72404660798---4"><img src="https://cdn-images-1.medium.com/max/2300/1*Fr9tiYzJN-fTfi4eAe4Nvg.png" width="2300"></a></p><p class="medium-feed-snippet">Discover 22 practical Android APK optimization tips, from R8 and unused dependencies to fonts, resources, and native libraries.</p><p class="medium-feed-link"><a href="https://proandroiddev.com/your-android-apk-is-bigger-than-it-needs-to-be-22-smart-ways-to-reduce-its-size-d39f678e5b43?source=rss----c72404660798---4">Continue reading on ProAndroidDev »</a></p></div>]]></description>
            <link>https://proandroiddev.com/your-android-apk-is-bigger-than-it-needs-to-be-22-smart-ways-to-reduce-its-size-d39f678e5b43?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/d39f678e5b43</guid>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[android-development]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Swatiomar]]></dc:creator>
            <pubDate>Fri, 13 Nov 2026 16:19:44 GMT</pubDate>
            <atom:updated>2026-11-13T16:19:42.915Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Inside Jetpack Compose’s $changed Bit Field: Static Arguments and Fewer Runtime Comparisons]]></title>
            <link>https://proandroiddev.com/jetpack-compose-compiler-internals-24370c24a891?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/24370c24a891</guid>
            <category><![CDATA[jetpack-compose]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Ravi Rawal]]></dc:creator>
            <pubDate>Wed, 11 Nov 2026 19:25:42 GMT</pubDate>
            <atom:updated>2026-11-11T19:25:41.445Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*stO5niG7F4r3sRf86AZ3OA.png" /></figure><h4>A closer look at parameter-state encoding, compiler IR analysis, comparison propagation, SlotTable storage, and the optimizations that happen before recomposition decisions reach the runtime.</h4><p>Jetpack Compose doesn’t need to compare every argument passed to every composable function.</p><p>The compiler already knows that some arguments cannot change. Consider a simple composable:</p><pre>@Composable<br>fun ProductCard(<br>    title: String,<br>    isAvailable: Boolean<br>) {<br>    ProductCardContent(<br>        title = title,<br>        isAvailable = isAvailable,<br>        maxLines = 2,<br>        showDivider = false<br>    )<br>}</pre><p>Four arguments are passed to ProductCardContent, but they don&#39;t all have the same characteristics.</p><p>title and isAvailable originate from the parent composable&#39;s parameters. Their change status can potentially be inherited from the parent. maxLines and showDivider, on the other hand, are fixed expressions.</p><p>There is no reason for the child composable to compare 2 against a previously stored 2, or false against a previously stored false.</p><p>These expressions can be identified at compile-time by the <strong>Compose compiler</strong> and stored in a synthetic parameter of type integer called $changed.</p><p>This means that the compiler doesn’t just pass argument values. It passes information about those values.</p><p>Understanding how this works will show us two very similar optimizations.</p><blockquote>Static argument elimination and comparison propagation.</blockquote><p>Both of which reduce the number of comparisons performed at runtime and the amount of storage space required in the SlotTable for detecting parameter changes.</p><p>The implementation details below are based on the AndroidX Compose compiler source. The generated signatures and bit structure are an internal part of the compiler and therefore can vary between releases.</p><h3>1. Why Compose needs a parameter change-tracking mechanism</h3><p>A composable function is not an ordinary function call from the runtime’s perspective.</p><p>Consider:</p><pre>@Composable<br>fun UserProfile(name: String) {<br>    Text(text = name)<br>}</pre><p>The compiler transforms composable functions to communicate with the Compose runtime. Among other things, it introduces synthetic parameters such as:</p><ul><li>A Composer reference.</li><li>A $changed bitmask describing the state of the arguments.</li><li>Additional changed masks when the parameter count requires them.</li><li>Default-argument metadata where applicable.</li></ul><p>Conceptually, the transformed function resembles:</p><pre>fun UserProfile(<br>    name: String,<br>    $composer: Composer&lt;*&gt;,<br>    $changed: Int<br>)</pre><p>This is illustrative pseudocode, not the exact generated JVM signature.</p><p>The runtime will need to know if the parameters of the function have changed since last run. If this information is not available from the caller then the generated code can call Composer.changed() to compare the current value with the previous stored value.</p><p>That comparison requires storage in the SlotTable.</p><p>The SlotTable stores all information related to the composition including previous values used by the runtime (e.g. parameter comparison and remember).</p><p>The crucial observation is that this comparison is only necessary when the compiler and caller don’t already know the answer.</p><p>This is precisely what $changed communicates.</p><p>We can look at the implementation in the ComposableFunctionBodyTransformer in the <a href="https://android.googlesource.com/platform/frameworks/support/+/b815b4040523b3ed89496b6a25219bd8543260f6/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt">compiler</a>. This handles the composable skipping, default arguments and comparison propagation.</p><h3>2. Anatomy of the $changed bit field</h3><p>The $changed parameter is an integer bitmask. It packs multiple pieces of information about the composable&#39;s parameters.</p><p>In the implementation examined, the compiler uses:</p><pre>const val BITS_PER_INT = 31<br>const val SLOTS_PER_INT = 10<br>const val BITS_PER_SLOT = 3</pre><p>Why 31 bits instead of 32?</p><p>The lowest bit is used to represent a function level flag which can be used to force execution. The next 30 bits are used to store ten 3-bit values representing parameters.</p><p>The effective layout is:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n69y2y88_sI8e7zELzbyEg.png" /><figcaption>32-bit integer, Bit 0: force execution, Bit 31: unused</figcaption></figure><p>More precisely:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L7sXkj4D7hwyhA6OwxkghQ.png" /></figure><p>The compiler calculates a parameter’s offset using:</p><pre>fun bitsForSlot(bits: Int, slot: Int): Int {<br>    val realSlot = slot % SLOTS_PER_INT<br>    return bits shl (realSlot * BITS_PER_SLOT + 1)<br>}</pre><p>A function with more than ten tracked parameter slots requires additional changed-mask integers.</p><p>Three bits of data are allocated to each parameter. <strong>However only 2 bits are used to indicate the comparison state.</strong></p><p><strong>The remaining bit carries stability metadata.</strong></p><h3>The two-bit comparison state</h3><p>The comparison states are:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1eOlI8LGTzAHktuwTFSlgw.png" /></figure><p>The third bit is used to represent instability where required:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jmkDwEaH-a9QaNb-UIpkWA.png" /></figure><p>The compiler’s StabilityBits implementation defines:</p><pre>UNSTABLE = 0b100<br>STABLE   = 0b000</pre><p>Consequently, Static is encoded as 0b011 within the three-bit slot, with the stable marker remaining clear.</p><p>For parameter slot zero, the actual shifted value is:</p><pre>Static = 0b011 shl 1<br>       = 0b00000110</pre><p>This compact representation allows the compiler to communicate both comparison state and stability information without introducing an object for every parameter.</p><h3>3. How the compiler determines that an argument is static</h3><p>The optimization begins before runtime execution, it happens at compile time.</p><p>The Compose compiler works with Kotlin Intermediate Representation (IR), and during its lowering passes, it examines the expressions being passed to composable functions, collects metadata about the arguments.</p><p>The relevant implementation uses structures named CallArgumentMeta and ParamMeta.</p><p>Conceptually, CallArgumentMeta records:</p><pre>data class CallArgumentMeta(<br>    var stability: Stability,<br>    var isVararg: Boolean,<br>    var isProvided: Boolean,<br>    var isStatic: Boolean,<br>    var paramRef: ParamMeta?<br>)</pre><p>This is a simplified representation of the compiler’s actual implementation.</p><p>The compiler examines the argument expression and determines:</p><ol><li>Whether its value can be established as static.</li><li>Whether it originates from an enclosing composable parameter.</li><li>Whether its stability is known.</li><li>Whether it was explicitly provided or comes from a default expression.</li><li>Whether the argument is a vararg.</li></ol><p>The static-expression analysis is implemented in AbstractComposeLowering.kt, particularly through IrExpression.isStatic().</p><h3>Expressions the compiler can classify as static</h3><p>The implementation recognizes several categories.</p><p><strong>Compile-time constants</strong></p><pre>Text(<br>    text = &quot;Hello&quot;,<br>    maxLines = 2<br>)</pre><p>IR constant expressions can be classified as static.</p><p><strong>Enum values</strong></p><pre>alignment = AlignmentOption.Center</pre><p>Enum entries are treated as static.</p><p><strong>Immutable local variables</strong></p><pre>val maxLines = 2<br><br>Text(<br>    text = title,<br>    maxLines = maxLines<br>)</pre><p>An immutable local variable can inherit static classification from its initializer.</p><p>A mutable variable doesn’t receive the same treatment.</p><p><strong>Stable object and companion-object references</strong></p><p>The compiler can classify object references as static when the relevant stability conditions are satisfied. Companion-object references receive special handling.</p><p><strong>Stable property access</strong></p><p>Certain read-only properties with default getters and stable return types can be classified as static, provided their receivers are also static. Stable annotations can also participate in this analysis.</p><p><strong>Operations on static values</strong></p><p>The compiler recognizes selected Kotlin operations, including arithmetic and Boolean operations, when the operation, result type, and operands satisfy its staticness conditions.</p><p>For example, a computation involving only constant operands can be classified as static.</p><p>The compiler also has specialized handling for certain function calls, including eligible stable functions with stable return types and static arguments.</p><p>This is not general-purpose constant folding. The compiler uses specific structural and stability rules to establish that an expression is safe to treat as static.</p><p>A dynamic state read remains dynamic.</p><pre>val currentMaxLines = uiState.value.maxLines<br><br>Text(<br>    text = title,<br>    maxLines = currentMaxLines<br>)</pre><p>The fact that currentMaxLines is declared using val doesn&#39;t make it static. Its initializer depends on observable runtime state.</p><p>This is an important distinction: <strong>immutability of a local reference is not proof that its value is constant.</strong></p><h3>4. Comparison propagation: avoiding repeated work across composable calls</h3><p>Static argument elimination is only half of the optimization.</p><p>The compiler also propagates parameter comparison results through the composable call graph.</p><p>Return to the earlier example:</p><pre>@Composable<br>fun ProductCard(<br>    title: String,<br>    isAvailable: Boolean<br>) {<br>    ProductCardContent(<br>        title = title,<br>        isAvailable = isAvailable,<br>        maxLines = 2,<br>        showDivider = false<br>    )<br>}</pre><p>The compiler can identify that title and isAvailable directly reference parameters of ProductCard.</p><p>It doesn’t need to make ProductCardContent independently compare those values if ProductCard already knows their change status.</p><p>The ParamMeta structure contains the parent parameter&#39;s slot index and a reference to the relevant changed or dirty mask.</p><p>When constructing the child’s changed mask, the compiler can extract the parent’s parameter state and move it into the child’s corresponding slot.</p><p>The logic is effectively:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*CpDjCSBc_AubSJ-mSkHqCg.png" /></figure><p>The actual implementation is more involved because it handles slot offsets, multiple masks, stability metadata, default arguments, and different expression types.</p><p>The important operation is in buildChangedArgumentsForCall().</p><p>For each argument, the compiler decides whether to:</p><ul><li>Set a static state directly.</li><li>Propagate a known parameter state from the parent.</li><li>Mark the argument uncertain.</li><li>Handle varargs or omitted defaults through their specialized paths.</li></ul><p>The resulting child mask is assembled using constants, bitwise operations, and shifts.</p><p>This prevents re-doing the same comparison at each level of a composable hierarchy.</p><p>We can split these into two useful categories:</p><ul><li>Static classification: The compiler can prove that an expression will never change.</li><li>Comparison propagation: The compiler propagates a comparison result (which has been proven) from one composable to another.</li></ul><p>Both reduce the number of comparisons, but use different information.</p><h3>5. What happens inside the composable function</h3><p>The receiving composable uses the incoming $changed mask to decide whether any parameter comparisons remain necessary.</p><p>Consider the conceptual logic:</p><pre>if (parameterState == Uncertain) {<br>    parameterState =<br>        if (composer.changed(value)) {<br>            Different<br>        } else {<br>            Same<br>        }<br>}</pre><p>This is simplified pseudocode, not literal generated source.</p><p>The important detail is that Composer.changed() is called only when the incoming state requires a comparison.</p><p>When called, the runtime compares the current value against its previously recorded value and updates the SlotTable bookkeeping.</p><p>The resulting state is reflected in the local $dirty mask.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D_oBQv8Xhu_gedC6CT7PsA.png" /></figure><p>Three cases have different consequences:</p><p><strong>Uncertain: </strong>A comparison is required. The runtime needs the previous value to establish whether the parameter changed.</p><p><strong>Same or Different: </strong>The caller already performed the necessary comparison. The callee can use the propagated result without performing another comparison or retaining a duplicate previous value for this purpose.</p><p><strong>Static: </strong>No comparison is necessary at all. The compiler has established that the argument cannot change.</p><p>This is why the optimization saves more than the cost of comparing two integers or references. It can also eliminate the parameter-specific SlotTable entry used solely for change detection.</p><p>The distinction is documented in the compiler source comments: the uncertain state is the one that can require SlotTable space for the comparison.</p><h3>6. Static default arguments are another optimization opportunity</h3><p>Composable functions frequently use default parameters:</p><pre>@Composable<br>fun ProductTitle(<br>    title: String,<br>    maxLines: Int = 2,<br>    color: Color = LocalContentColor.current<br>) {<br>    Text(<br>        text = title,<br>        maxLines = maxLines,<br>        color = color<br>    )<br>}</pre><p>The compiler handles default values for composable functions with the use of a separate $default bitmask.</p><p>This mask must not be confused with $changed.</p><ul><li>The $default bitmask will tell you which arguments were not provided.</li><li>The $changed bitmask tells you about the parameter comparison and stability.</li></ul><p>The default mask has one bit per parameter to indicate if the default expression needs to be evaluated.</p><p>The compiler can also optimize the default expression.</p><p>For maxLines = 2, the compiler knows the default is static. If the caller omits the argument, the resulting parameter can be assigned the Static state.</p><p>For color = LocalContentColor.current, the result depends on the composition environment. It cannot simply be treated as an immutable constant.</p><p>The compiler must preserve the behavior of dynamic defaults, including the possibility that they contain composable calls or observable state reads.</p><p>This also affects generated composition groups. When default expressions require runtime evaluation, the compiler may generate a defaults group to ensure that they are reevaluated correctly when necessary. When all defaults are static, it can avoid some of that machinery.</p><p>This is a useful example of how a seemingly minor expression-level optimization can influence the generated composition structure.</p><h3>7. Static does not mean stable, and neither guarantees skipping</h3><p>There are three concepts that need to remain separate.</p><p><strong>Static</strong> describes an argument expression that cannot change during program execution.</p><p><strong>Stable</strong> describes whether Compose can reliably determine changes to an object’s observable properties.</p><p><strong>Skippable</strong> describes whether a composable can avoid executing its body when called during recomposition and its relevant inputs remain unchanged.</p><p>For example:</p><pre>@Composable<br>fun UserName(name: String) {<br>    Text(<br>        text = name,<br>        maxLines = 1<br>    )<br>}</pre><p>String is a stable type, but name can change between compositions.</p><p>maxLines is static because it is fixed.</p><p>The composable’s ability to skip execution is a separate decision involving its other parameters, restartability, invalidations, and runtime state.</p><p><a href="https://developer.android.com/develop/ui/compose/performance/stability/strongskipping">Strong skipping</a>is an extra dimension. Enabled by default from Kotlin 2.0.20 onwards. If a composable is restartable, it can be skipped even if it contains unstable parameters. Parameters will be compared using structural equality if stable, or instance equality if strong skipping is enabled.</p><p>Static arguments are another optimisation. The compiler does not need to compare parameters since it can confirm the argument will never change.</p><p>This does not stop recompositions happening due to a State being read within the composable, or the CompositionLocal changing or some other invalidation.</p><p>The optimization concerns parameter change detection, not every possible reason a composable might execute.</p><h3>8. How to inspect static argument optimization in your project</h3><p>The compiler exposes reports and metrics that help investigate how it analyzes composables.</p><p>For Kotlin projects using the Compose compiler Gradle plugin, enable reports and metrics:</p><pre>composeCompiler {<br>    reportsDestination =<br>        layout.buildDirectory.dir(&quot;compose_compiler&quot;)<br><br>    metricsDestination =<br>        layout.buildDirectory.dir(&quot;compose_compiler&quot;)<br>}</pre><p>The generated reports include several useful artifacts.</p><p><strong>module.json</strong>: Contains aggregate metrics such as:</p><pre>{<br>  &quot;staticArguments&quot;: 25,<br>  &quot;certainArguments&quot;: 138,<br>  &quot;knownStableArguments&quot;: 377,<br>  &quot;knownUnstableArguments&quot;: 25,<br>  &quot;totalArguments&quot;: 426<br>}</pre><p>These values illustrate the types of metrics available. They are not measurements from a particular application.</p><p>staticArguments counts arguments identified as static at composable call sites.</p><p>certainArguments provides information about arguments for which the compiler can propagate comparison metadata.</p><p><strong>composables.txt</strong>: Provides composable signatures with parameter stability and default-expression classifications.</p><p>For example, the output can distinguish:</p><pre>stable maxLines: Int = @static 2<br>stable color: Color = @dynamic ...</pre><p>This helps identify cases where a default expression is treated as dynamic rather than static.</p><p><strong>composables.csv</strong> : Contains function-level metrics that can be inspected programmatically or compared between builds.</p><p>The <a href="https://github.com/JetBrains/kotlin/blob/master/plugins/compose/design/compiler-metrics.md">compiler metrics</a> documentation explains the report formats and their interpretation:</p><p>One limitation matters: these reports don’t expose every generated $changed bit pattern at every call site. They provide compiler-analysis information, not a runtime trace of all bit-level decisions.</p><p>For deeper investigation, inspect the compiler’s IR transformation and generated bytecode for a small, isolated example. Avoid depending on synthetic parameter positions or bit encodings in application code.</p><h3>The optimization is about eliminating information the runtime no longer needs</h3><p>The static bit is an aspect of a larger change-tracking mechanism.</p><p>The compiler categorizes expressions, tracks parameter states via composable calls, and then encodes them as compact integer bit masks. The runtime checks for value equality only if the incoming metadata does not provide a clear outcome.</p><p>This means two separate benefits:</p><ol><li>Static expressions do not require the runtime to compare parameters or store prior value states for change checking.</li><li>If values have been checked by the caller, there is no need to check them multiple times at each composable.</li></ol><p>Neither optimization ensures that a composable is skipped from execution. However, both decrease the effort needed to make that decision.</p><p>For Android developers evaluating Compose performance, this is an important consideration. Performance of recomposition is not dictated by the number of composables executed but also by how effectively the compiler and runtime determine whether those executions are necessary.</p><p>The $changed mask is one of the mechanisms that makes that possible.</p><h3>Sources</h3><ul><li><a href="https://android.googlesource.com/platform/frameworks/support/+/b815b4040523b3ed89496b6a25219bd8543260f6/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt">Parameter states, mask encoding, comparison propagation, default processing and skipping</a></li><li>IrExpression.isStatic() <a href="https://android.googlesource.com/platform/frameworks/support/+/6c760e5518d5f0dd58f0391cbb01920b62f97902/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt">and the compiler&#39;s static-expression classification</a></li><li><a href="https://android.googlesource.com/platform/frameworks/support/+/f2e05c341382db64d127118a13451dcaa554b702/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ClassStabilityTransformer.kt">Stability-bit representation and generated stability metadata</a></li><li>staticArguments, certainArguments, <a href="https://github.com/JetBrains/kotlin/blob/master/plugins/compose/design/compiler-metrics.md">and compiler report formats</a></li><li><a href="https://developer.android.com/jetpack/androidx/releases/compose-runtime">Historical documentation of comparison propagation and static parameter optimization</a></li><li><a href="https://github.com/androidx/androidx/blob/androidx-main/compose/runtime/design/how-compose-works.md">SlotTable and composition implementation</a></li><li><a href="https://developer.android.com/develop/ui/compose/performance/stability">Stable and unstable parameter comparison behavior</a></li><li><a href="https://developer.android.com/develop/ui/compose/performance/stability/strongskipping">Skippable and stable versus unstable equality checks</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=24370c24a891" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/jetpack-compose-compiler-internals-24370c24a891">Inside Jetpack Compose’s $changed Bit Field: Static Arguments and Fewer Runtime Comparisons</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to get 12 testers for Google Play closed testing (and actually pass)]]></title>
            <link>https://proandroiddev.com/how-to-get-12-testers-for-google-play-closed-testing-and-actually-pass-6a2ab52a0779?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/6a2ab52a0779</guid>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[google-play]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[David Kirsch]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 16:15:05 GMT</pubDate>
            <atom:updated>2026-11-09T16:15:04.370Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p71Q2TTj6Tx3Q1lZF_Rb-A.png" /></figure><p>When I finished my app SnapReturn this fall, I hit the same wall a lot of indie Android developers hit: Google wouldn’t let me publish to production until I ran a closed test with at least 12 testers for 14 days. I’m an Android developer by day, so I figured this part would be easy. It wasn’t.</p><p>I got one friend and three family members. My Reddit posts got a few replies, and some of those people disappeared after a day or two. What finally got me through was a group of developers testing each other’s apps. Here’s everything I learned along the way.</p><h3>The rule, in 30 seconds</h3><p>If your personal Play Console account was created after November 13, 2023, you need to run a closed test before you can publish to production. It needs at least <strong>12 testers</strong> who stay opted in <strong>continuously for at least 14 days</strong>. Then you answer a set of questions about your test and wait for Google to review your application.</p><ul><li>The 14 days must be consecutive, and testers who leave early don’t count.</li><li>A tester who opts out and back in starts their count again.</li><li>Organization accounts don’t have this rule.</li></ul><p>Google’s rules can change, so check <a href="https://support.google.com/googleplay/android-developer/answer/14151465?hl=en">Google’s help page</a> for the latest.</p><h3>Why “12 opted in” isn’t enough</h3><p>The number is the easy part to understand. The part that trips people up is engagement. When you apply for production, Google asks how engaged your testers were, what feedback they gave, and what you changed because of it. Developers regularly report being asked to keep testing even after reaching 12, because their testers barely opened the app.</p><p>So the real goal isn’t 12 names on a list. It’s 12+ people who actually use your app, often enough that your answers to Google are true and specific.</p><h3>Where to find testers</h3><p><strong>Recruit 15–20, not 12.</strong> Someone always drops out, and a spare tester only helps if they opted in from the start.</p><h3>Set up your test so nobody gets stuck</h3><ul><li><strong>Use a Google Group for testers.</strong> Anyone can join it, and you add the group to your closed track once, instead of collecting everyone’s email.</li><li><strong>Make the test available in all countries.</strong> If it’s limited to your country, testers elsewhere can’t install it, and they won’t always tell you.</li><li><strong>Share the “Join on the web” link</strong> from your closed track, and remind people to use the same Google account for the group and the opt-in.</li><li><strong>Publish the release</strong> and wait for Google’s review of the closed track before you send the link. An unreviewed track shows testers an error.</li></ul><h3>Keep testers engaged for 14 days</h3><ul><li><strong>Give a small task each day.</strong> “Open the app” gets ignored. “Add a recipe from a website” gets done, and it gets the part you care about tested.</li><li><strong>Keep tasks short.</strong> A couple of minutes is the sweet spot. Your testers are doing you a favor.</li><li><strong>Ask for one sentence of feedback</strong>: what worked, and what got in the way. Short notes add up to real evidence.</li><li><strong>Ship at least one update during the test</strong>, and tell testers what you fixed. It shows Google the test changed your app.</li><li><strong>Check Play Console’s tester count</strong> every few days. It’s the number Google uses.</li></ul><h3>Write down what happens as you go</h3><p>When you apply for production, Google asks about three things:</p><ul><li><strong>Your closed test:</strong> how you recruited testers, whether they used all your features, and what feedback they gave</li><li><strong>Your app:</strong> who it’s for, its value, and how many installs you expect in the first year</li><li><strong>Your readiness:</strong> what you changed after testing, and how you decided the app was ready</li></ul><p>It’s much easier to answer honestly if you kept notes: which features got tested, the problems testers found, and what you fixed. Specific, true answers hold up. Vague or exaggerated ones don’t.</p><h3>The short version</h3><ol><li>Recruit 15–20 testers, not 12.</li><li>Use a Google Group and make the test available in all countries.</li><li>Give testers a short task every day.</li><li>Fix what they find, ship an update, and keep notes.</li><li>Answer Google’s questions with what really happened.</li></ol><p>After going through this myself, I built <a href="https://getfourteen.app/"><strong>Fourteen</strong></a>, where groups of 16 Android developers test each other’s apps daily for 14 days, with short tasks and an optional SDK that confirms real use. It also drafts your answers to Google’s questions from your test data. The first cohort begins Saturday, November 14, 2026. Whatever route you take, I hope this saves you some of the time it cost me.</p><p>More guides: <a href="https://getfourteen.app/production-access-rejected">Why production access gets rejected</a> · <a href="https://getfourteen.app/production-access-questions">How to answer the production access questions</a></p><p><em>Originally published at </em><a href="https://getfourteen.app/how-to-get-12-testers"><em>https://getfourteen.app</em></a><em> on November 8, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6a2ab52a0779" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/how-to-get-12-testers-for-google-play-closed-testing-and-actually-pass-6a2ab52a0779">How to get 12 testers for Google Play closed testing (and actually pass)</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI Coding for Android: Reflections and Building Self-Verification]]></title>
            <link>https://proandroiddev.com/ai-coding-for-android-reflections-and-building-self-verification-2ca1d2f598a7?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/2ca1d2f598a7</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[context-engineering]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[harness-engineering]]></category>
            <dc:creator><![CDATA[rayford]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 14:54:49 GMT</pubDate>
            <atom:updated>2026-11-09T14:54:48.633Z</atom:updated>
            <content:encoded><![CDATA[<p>AI Coding is rapidly evolving from code completion into an end-to-end development workflow spanning requirements analysis, solution planning, implementation, testing, and bug fixing. For Android development, this is not simply a matter of replacing an IDE autocomplete tool with a Coding Agent. It means redesigning the engineering environment in which an Agent can work reliably: it must understand the business context, respect client architecture boundaries, run builds and tests, and correct itself when something fails.</p><p>Although AI can do more and more for engineers, it will not replace them. Coding has always been only one part of software development, and engineers are more likely to evolve from people who implement code into people who use AI to build the entire development system. Rather than panic, we can keep learning and evolve alongside AI.</p><h3>What Agentic Engineering Does</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g1LuBJw3MlXhyqT7" /></figure><p>(Figure from <a href="https://martinfowler.com/articles/harness-engineering.html">Harness engineering for coding agent users</a>)</p><p>An AI Coding system usually has three components: a foundation model, a Coding Agent tool, and the context, skills, and verification system supplied by the engineering team. All three affect the quality of the final deliverable.</p><p>The foundation model sets the upper bound for reasoning, code comprehension, and generation. As models such as Claude, GPT, and Gemini improve, their first-pass success rate on complex tasks generally rises, but so does the cost. In practice, teams trade off cost, response time, and reliability. Even with a top-tier model, complex business requirements, cross-module refactoring, and performance investigations cannot be completed reliably in a single generation pass. This is especially true in Android projects: Gradle builds, build variants, resource and manifest merging, Jetpack component lifecycles, JNI/NDK, R8 obfuscation, app startup paths, and UI rendering all increase the Agent’s comprehension burden.</p><p>Coding Agent tools organize model capabilities into an executable development loop. Tools such as Claude Code, Codex, and CodeBuddy include system prompts, file search, editing, command execution, task decomposition, and context compaction. Some tools also train models in real tool-use environments, making them better at reading repositories, changing code, running tests, and repairing failures. This surrounding system is commonly called the <em>harness</em>: the execution environment around the model.</p><p>The context and skills supplied by engineers determine whether the Agent acts correctly in a particular project. For an Android team, that context includes the app’s business goals, module boundaries, architecture layers, shared components, design system, build procedures, test commands, performance budgets, staged rollout rules, privacy and compliance requirements, and directories or interfaces that must not be changed casually.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pOud1Exa58OsLn_k" /></figure><p>(Figure Inspair by <a href="https://martinfowler.com/articles/harness-engineering.html">Harness engineering for coding agent users</a>)</p><p>To enable high-quality AI delivery, engineers must do at least two things: guide the Agent to work with the right context, and enable it to verify that its output is correct. These correspond to Context Engineering and Harness Engineering, respectively.</p><p>Context Engineering asks, “What does the Agent see when making decisions?” A large language model is essentially stateless: each output is determined by the current context window. When context is accurate, complete, and concise, the Agent makes more reliable judgments. When context is disorganized, outdated, or noisy, even a strong model may generate code from faulty premises.</p><p>Harness Engineering asks, “How does the Agent know it got the job done right?” For Android, delivery cannot be judged solely by whether code was generated. It must also compile, preserve architecture constraints, pass critical test cases, conform to UI specifications, avoid performance regressions, and include the required analytics instrumentation and staged rollout strategy. Without such feedback, an Agent can mistake “I wrote a lot of code” for “the task is complete.”</p><h3>From an Agent Software Factory to a Controlled Development Process</h3><p>If context, tools, and verification are taken to their limits, can AI Agents do all the work of software development themselves? That is the premise of the AI Agent software factory.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HOpK-OSq4N9_wZ_x" /></figure><p>(Figure from <a href="https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/wsff.md">Why the Software Factory Failed</a>)</p><p>Some in the industry have already tried a lights-off software factory: requirements enter a queue, and Agents automatically implement, test, fix, and commit changes, sometimes even handling production feedback. Such systems can look highly efficient in the short term because code generation really is faster than manual coding. But in a complex production codebase, a process where nobody reads the code or reviews the design is not sustainable.</p><p>For now, think of an Agent as a new engineer joining the team. Writing prompts, describing requirements, and assigning tasks amount to communicating requirements remotely. <strong>Even communication between people has information gaps</strong>: the business background, implicit constraints, and trade-offs in a requester’s head are hard to convey completely to the implementer, and written records inevitably contain omissions and ambiguities. <strong>The problem is more pronounced for an Agent, which can only infer the system’s intent from its current context and tool results.</strong> Research and Plan should therefore be vehicles for iterative discussion, not one-off documents. During planning, engineers need to keep asking questions, adding constraints, and correcting misunderstandings until the scope of change, architectural direction, edge cases, and verification approach are confirmed.</p><p>Does an approved Plan guarantee good code? Still no. Even code written by an experienced engineer needs review. Agents make mistakes too, whether in the form of hallucinated assumptions, incorrect API usage, duplicated logic, excessive abstraction, or inappropriate fallbacks added merely to pass tests. After multiple iterations, a seemingly minor disturbance can propagate along a dependency chain and eventually produce a tangled codebase that is difficult to understand or change. Plan Review addresses whether the direction is right; Code Review addresses whether the implementation is right. Neither can replace the other.</p><p>The underlying issue is not that the prompt needs to be better, nor can it be solved by adding a few more loops. <strong>Most current coding benchmarks reward passing tests but struggle to penalize damage to maintainability. A model can pass tests with local hacks, duplicated logic, poor abstractions, excessive fallbacks, or workarounds that bypass the type system. The costs of those choices often emerge only weeks, months, or even a year later.</strong></p><p><strong>No matter how much efficiency AI brings, the fundamental problems of software architecture and software engineering do not disappear.</strong> How should modules be divided? How should dependencies be controlled, state managed, concurrency coordinated, errors propagated, and systems evolved? Faster code generation does not automatically answer any of these questions. Pitfalls engineers encountered decades ago still appear today in new forms. The communication costs, system complexity, organizational coordination, and design principles discussed in classics such as <em>The Mythical Man-Month</em>, <em>Peopleware</em>, and works on distributed systems are even more worth revisiting in the AI era. <strong>AI can shorten the path to implementation, but it cannot make architectural judgments for the team or bear the codebase’s long-term maintenance costs.</strong></p><p>Long-term maintainability is particularly important in Android codebases. A screen working today does not mean it handles lifecycle management, state restoration, resource cleanup, retries on poor networks, internationalization, accessibility, dark mode, foldables, and performance budgets correctly. A cross-platform dynamic UI approach may suit promotional pages or e-commerce campaign pages, but in a highly interactive utility app, an audio/video scenario, or an experience requiring a high frame rate, it may impose costs in time to first frame, scrolling, input latency, and native feel. Architectural trade-offs must be judged against the business model, user scale, team capabilities, and future evolution — not just immediate generation speed.</p><h3>Why Plans and Code Both Need Review</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7-55Pr2o-y2Kb4_L" /></figure><p>(Figure Inspire by <a href="https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md">https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md</a>)</p><p><strong>The earlier an error occurs, the greater its amplification effect.</strong> One bad line of code is a local problem; a flawed implementation plan can produce hundreds of lines of wrong code; a misunderstanding of the requirements or a faulty technical research conclusion can steer the entire feature in the wrong direction.</p><p>Human review in Agentic Engineering should therefore not be reserved for the final Code Review. The higher-leverage review points come earlier:</p><ul><li>During requirements analysis, confirm the user problem, success criteria, edge cases, and explicit exclusions.</li><li>During system architecture, confirm module boundaries, data flow, API contracts, threading model, and state management.</li><li>During program design, confirm core types, call chains, file layout, dependency direction, and error-handling strategy.</li></ul><p>For Android development, Plan Review should at least ask: Are we changing the right module? Is the code going in the right place? Are we using the right screen architecture? Do we need to add or update unit tests? Will the change affect app startup, the main thread, or memory usage?</p><h3>Where Humans Must Stay Involved</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1vUyCYMLVQQYlQbK" /></figure><p>(Figure Inspire by <a href="https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md">https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md</a>)</p><p>A workflow suitable for Agents can be divided into six steps:</p><ol><li>Requirements or problem analysis: Input may be a feature request, bug report, production alert, user feedback, or test case. The output should clarify the business goal, reproduction steps, impact, and acceptance criteria.</li><li>Codebase research: Have the Agent read the relevant modules, call chains, historical implementations, and test baselines, and produce a research document rather than changing code immediately.</li><li>Solution planning: Based on the research, produce a plan that identifies which files to change, why, how to verify the changes, and where the risks lie.</li><li>Human review of the plan: Engineers focus on whether the direction is correct, abstractions are appropriate, and verification is sufficient.</li><li>Staged implementation: The Agent implements the plan, running a build, tests, or device verification after each runnable increment.</li><li>Human review of code and reports: Engineers inspect key diffs, verification results, and uncovered risks, then decide whether to extend the work, roll it back, or hand it over for testing.</li></ol><p>This process does not remove people from development. <strong>Instead, it asks engineers to shift their attention from repetitive coding to high-value judgment: clarifying requirements, making architectural trade-offs, enforcing quality gates, and accepting deliverables.</strong></p><h3>Context Engineering: Helping the Agent Work with the Right Context</h3><p>Large language models do not have true long-term memory. For a Coding Agent, the current context window is the primary basis for understanding a project. Context management is not simply a matter of “feeding it more material”; it requires controlling information quality — accuracy, completeness, concision, and loading information on demand.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iYCKyIS2ZzE5DRyl" /></figure><p>(Figure from <a href="https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md">https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md</a>)</p><p>A good AGENTS.md, CLAUDE.md, or project-level guide should answer the questions an Android Agent most often encounters:</p><ul><li>What does the app do, and what are its core user journeys?</li><li>How are the Gradle modules organized, and in which direction do dependencies run among App, Feature, Core, Data, Design System, and Native modules?</li><li>Which architecture is used — for example, MVVM, MVI, Clean Architecture, Redux-like state management, or a custom framework?</li><li>What is the UI stack — View System, Jetpack Compose, or both — and how should themes, color palettes, component libraries, and design tokens be used?</li><li>How is the data layer organized — for example, Repository, UseCase, Room, DataStore, networking SDKs, caching policies, and offline support?</li><li>What are the build and verification entry points — for example, common variants, assemble tasks, lint, unit tests, on-device tests, screenshot tests, NDK builds, and native unit tests?</li></ul><p>This file needs regular maintenance, but it must not grow without bound. Stuffing every module detail into the initial context consumes the model’s attention budget. Even with a large context window, too much irrelevant information diffuses attention and makes mistakes on critical decisions more likely.</p><p><strong>A better approach is layered context: keep only a project map and navigation index at the root; record local rules in module-level documents; and put specialized knowledge about builds, tests, performance, releases, analytics, privacy, and other topics in dedicated documents. The Agent reads only the top-level guide at the start of a task, then follows the index to the relevant documents when it enters a module.</strong></p><p>Context Engineering also requires frequent, deliberate context compaction. Code search results, build logs, failed attempts, long JSON payloads, and test output can all pollute the context. In a complex task, the Agent should compress noisy information into structured artifacts at each Research, Plan, and Implement stage: which files have been read, what it currently understands, which options were ruled out, what risks remain, and how to verify the next step. That way, the next Agent — or a human taking over — does not have to reconstruct the state from a chat history.</p><p>Rules, skills, MCP integrations, and external tools also consume context and tokens. Do not load every capability up front unless necessary. Load progressively: bring in build instructions when the task requires an Android build, and device-operation and testing skills only when UI verification is needed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UYbCtcGESvov1VJK" /></figure><h3>Harness Engineering: Defining What Counts as Done</h3><p>Context answers, “Does the Agent know what to do?” Harness answers, “How does the Agent know it has done it correctly?”</p><p>In an Android project, harness controls fall into two categories:</p><ul><li>Feedforward controls: Constraints and guidance supplied before the Agent acts, such as architecture rules, module boundaries, coding standards, shared-component usage guides, and testing requirements.</li><li>Feedback controls: Executable feedback supplied after the Agent acts, such as Gradle builds, Android Lint, Detekt/Ktlint, unit tests, Compose UI Tests, Robolectric, instrumentation tests, NDK builds, gtest, and static architecture checks.</li></ul><p>Checks with deterministic, CPU-executable outcomes should be moved as early as possible. These include compilation, lint, type checks, unit tests, module-dependency checks, complexity checks, and coverage checks. Semantic judgments — whether a design is overengineered, tests cover the core business behavior, or a UI meets product expectations — can be supported by AI review, but engineers must still make the final call.</p><p>The point of a harness is not to dictate a rigid path to the Agent, but to define the desired deliverable clearly. Too many process constraints limit the model’s ability to discover better solutions. As model capabilities improve, yesterday’s protective procedures can become obstacles. <strong>A more robust approach is to prescribe fewer step-by-step instructions, provide more capabilities, boundaries, and acceptance criteria, let the Agent plan its own work, and catch mistakes with executable verification.</strong></p><h3>Improving Agent Output Quality</h3><p>Once a Coding Agent generates code, it must first pass engineering-level verification. For Android, at a minimum, the target variant must compile, and all relevant Kotlin/Java code, resources, manifests, and native code must pass through the build pipeline. Modules involving C++ also need NDK compilation and native unit tests. UI changes need checks for shared color palettes, shared components, consistent spacing, the theme system, and accessibility properties.</p><p>But a successful build is only the lowest bar. Whether a client feature is actually correct depends on business logic, lifecycle handling, state restoration, thread switching, error handling, data consistency, and dependencies outside the client behaving as expected.</p><h3>Improving Testability with IoC and DI</h3><p>Unit testing is mature in the Android ecosystem, but much legacy code is difficult to test. Common problems include business logic scattered across Activities and Fragments; ViewModels that access global Managers directly; network, database, configuration, account state, and experiment flags read through singletons; utility classes that hide I/O; and Native and Kotlin layers coupled through interfaces that are difficult to replace.</p><p>Such code is not impossible to test; it is simply too expensive to test. <strong>To make a module testable, external dependencies must be separated from business logic and passed in through interfaces and constructor injection. This is the principle of Inversion of Control (IoC) and Dependency Injection (DI).</strong> On the Kotlin/Java side, teams can use Hilt, Dagger, Koin, or an in-house container. On the C++ side, dependencies can be wrapped using Fruit or a lightweight factory.</p><p>The value of dependency injection is not merely cleaner-looking code. It gives both Agents and engineers a stable way to replace dependencies. When testing a ViewModel, they can mock networking, databases, account state, experiment flags, and the time source. When testing a Repository, they can swap out remote services and local caches. When testing a Native ViewModel or business core, they can run fast local tests without an Android device.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kOT83PNcQtafCYh5" /></figure><p>In the era of manual coding, retrofitting DI and tests into legacy modules was often tedious. Coding Agents are well suited to this kind of well-defined refactoring: they can identify global dependencies, extract interfaces, adjust construction paths, add tests, and run verification at each step. Engineers should focus their reviews on whether interface boundaries make sense, whether testability has come at the expense of sound design, and whether excessive abstraction has been introduced.</p><h3>Layering Unit Tests and Client-Side Verification</h3><p>Android tests can be layered by feedback speed and fidelity.</p><p>JVM unit tests are the fastest. They are well suited to ViewModels, UseCases, Repositories, state machines, data transformations, business rules, and error handling. With dependency injection, an Agent can create or update tests for every requirements change, covering happy paths, edge cases, failures, and regressions.</p><p>Next come Robolectric and lightweight framework tests, which can validate some Android Framework-dependent logic but cannot fully replace a real device. Further down the stack are instrumentation tests, Compose UI Tests, UIAutomator, and screenshot tests, which validate screen behavior, interaction flows, and visual fidelity on real devices. Native logic should be verified quickly with local gtest runs whenever possible, rather than requiring a full app launch every time.</p><p>For screens built with Jetpack Compose, UI state should be driven by a stable state model, with Composables kept free of side effects — or limited to minimal ones — where possible. Then an Agent can build tests around state inputs and UI outputs instead of fragile tap sequences. For older View System screens, business decisions should likewise be moved out of View callbacks into a ViewModel or Presenter to make UI automation less brittle.</p><h3>Observed Results</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Wj5fksND9hOQGT57" /></figure><p>Take CreatePostViewModel.cpp, which handles editing topic posts. After a dependency injection refactor, a Coding Agent can enumerate and generate unit tests for the main business paths: drafts, topic selection, body validation, image state, publishing failures, retries, and recovery from errors. Each change runs native unit tests before moving to client integration verification. The real benefit is not &quot;more tests&quot;; it is catching low-level C++ logic errors before handoff to QA and giving engineers more confidence in letting the Agent change complex logic.</p><h3>Preventing Feature Gaps: From Unit Tests to Integration Acceptance</h3><p>Unit tests can protect client-side logic, but a requirement is never delivered simply because “the client code is done.” Complete delivery also depends on accounts, server APIs, configuration delivery, experiment flags, assets, operations consoles, analytics instrumentation, staged rollout, and test cases. Before handing a feature to product and QA, teams generally also need integration test cases to demonstrate that the primary flow works.</p><p>At its core, integration testing verifies end-to-end behavior: launch the full app, use a real or near-real account, perform a sequence of actions, and observe screen state and service responses. In other words, the system must “see what is on the device screen, perform the next action, and then judge whether the outcome matches expectations.”</p><p>The Android SDK already provides capabilities such as adb, UIAutomator, screenshots, accessibility trees, log capture, file retrieval, and Intent-based launching. Android CLI tools and community projects aimed at Coding Agents now package these capabilities into testing skills the Agent can call. Projects such as Trailblaze attempt to connect device control, screen understanding, test recording, and verification into one workflow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*volizxQSKZrKLiRH" /></figure><p>One important lesson from <a href="https://github.com/block/trailblaze">Trailblaze</a> is that device verification need not rely heavily on screenshots and OCR. Reading the UI hierarchy and accessibility information first can greatly reduce token usage while helping the model identify controls, text, and hierarchy more reliably. Screenshots and OCR are better used as fallbacks when the UI tree is unavailable or visual judgment is required. If verification targets Android only, the official Android CLI and Skills can also be used instead of <a href="https://github.com/block/trailblaze">Trailblaze</a> for device control and integration testing.</p><p>If a QA team maintains integration test cases, a practical Android Agent verification workflow might look like this: export integration test cases from a test management platform or XMind and convert them to structured Markdown; connect a test device and install the target build; have the Agent execute each case step by step; capture the UI tree, screenshots, and logs at each step; when a failure occurs, record the current screen, action history, key logs, and suspected cause; finally, produce a test report identifying passed, unexecuted, blocked, and human-review-required cases.</p><p>Alternatively, if nobody maintains integration test cases, tests can be created with <a href="https://developer.android.com/studio/gemini/journeys">Journeys</a> and run by the Agent using Android CLI.</p><p>To help the Agent execute those cases, provide a feature guide. It might be a user-facing wiki or a web page the Agent can access, explaining how to reach and operate the feature so the Agent does not have to guess its way through the app.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*rAMjx2YivoWwWMRC" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BK0Fp45PaeNC0KBW" /></figure><p>This approach can cover most stable primary flows, such as login, publishing, search, list refresh, form submission, configuration changes taking effect, and error messages. It still has limits: cross-device interactions, complex gestures, video playback experience, animation flicker, subtle visual fidelity, unstable network conditions, and perceived performance still require humans or specialized testing tools. The Agent’s value is to run the automatable integration paths first and organize clear failures and omissions, allowing engineers, QA, and product managers to focus on the problems that genuinely require judgment.</p><h3>Conclusion</h3><p>The diagram below summarizes the building blocks of an Android harness. Guides provide context and quality standards before coding. The Coding Agent performs Research, Plan, Implement, and Repair. Sensors provide feedback on builds, behavior, performance, and stability. The system then preserves test results, code diffs, and runtime metrics as evidence of delivery. Human Review sits at high-risk decisions and final acceptance; when tests fail or metrics regress, the feedback goes back to the Agent for correction.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dC8ylfjjYl5fyM8C" /></figure><p>The goal of Agentic Engineering is not to create an unsupervised software factory, but to help Android engineers deliver software with greater leverage. AI accelerates research, generates code, adds tests, runs verification, and compiles reports. Humans decide whether the requirements are right, the architecture is sound, the quality bar has been met, and which risks cannot be delegated to a model.</p><p>In an Android environment marked by long product lifecycles, demanding experience requirements, and fragmented devices, reliable AI Coding does not mean “getting the Agent to write more code.” It means “getting the Agent to write code with the right context and prove it is correct through executable verification.” Context Engineering determines whether the Agent understands the system; Harness Engineering determines whether it can validate the outcome; and human review determines whether the codebase remains healthy over time.</p><h3>References</h3><ul><li><a href="https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md">Advanced Context Engineering for Coding Agents</a></li><li><a href="https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/wsff.md">Why the Software Factory Failed</a></li><li><a href="https://martinfowler.com/articles/harness-engineering.html">Harness Engineering</a></li><li><a href="https://walkinglabs.github.io/learn-harness-engineering/en/lectures/lecture-08-why-feature-lists-are-harness-primitives/">Use Feature Lists to Constrain What the Agent Does</a></li><li><a href="https://github.com/walkinglabs/awesome-harness-engineering">Awesome Harness Engineering</a></li><li><a href="https://github.com/yzfly/awesome-context-engineering">Awesome Context Engineering</a></li><li><a href="https://github.com/block/trailblaze">Trailblaze</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2ca1d2f598a7" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/ai-coding-for-android-reflections-and-building-self-verification-2ca1d2f598a7">AI Coding for Android: Reflections and Building Self-Verification</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Migrating a Real Android App to Kotlin Multiplatform 3/4 — Sharing the UI without losing safety net]]></title>
            <link>https://proandroiddev.com/migrating-a-real-android-app-to-kotlin-multiplatform-3-4-sharing-the-ui-without-losing-safety-net-f74a6bf408ae?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/f74a6bf408ae</guid>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[androiddev]]></category>
            <dc:creator><![CDATA[Daniel Voko]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 14:53:26 GMT</pubDate>
            <atom:updated>2026-11-09T14:53:25.593Z</atom:updated>
            <content:encoded><![CDATA[<h3>Migrating a Real Android App to Kotlin Multiplatform</h3><h4>Part 3: Sharing the UI without losing the safety net</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qBy67NQd89PthDKkqu_PZw.png" /></figure><p><a href="https://proandroiddev.com/migrating-a-real-android-app-to-kotlin-multiplatform-2-4-what-moved-for-free-and-what-hurt-7ec973002f01"><strong>Part 2</strong></a><strong> </strong>ended on a pattern: the code that moved most easily was the code that knew nothing about Android. By that logic the UI layer should have been the worst part of the migration. It touches the screen, the system bars, the keyboard, the back gesture — everything that is unmistakably a platform.</p><p>And, much to my amazement, it wasn’t. The composables moved about as easily as the domain models did.</p><p>What didn’t move was everything around them. The resource system, the build plugins, the preview tooling and the screenshot tests were all built on the assumption that a Compose module is an Android library module. Once that stopped being true, the application code was fine and the ecosystem around it wasn’t.</p><p><strong>Sharing application code turned out to be easier than sharing the ecosystem around it.</strong> That’s the whole article.</p><h3>What moved easily</h3><p>Most composables moved to commonMain unchanged. Layout, state, theming, animation — Compose Multiplatform covers the surface <a href="https://bike-hub.app/"><strong>BikeHub</strong></a> uses, and the diff for a typical screen was mostly imports.</p><p>Two things made this smoother than it would otherwise have been.</p><p><strong>The UDF layer went first.</strong> Every feature module in BikeHub uses <a href="https://github.com/get-bridge/ouroboros-compose">Ouroboros-Compose</a>, a small unidirectional-data-flow library (influenced by <a href="https://github.com/spotify/mobius">Spotify’s Mobius</a>, that I used back then, when views and fragments ruled the android UI). It abstracts the ViewModel away and keeps the UI logic in plain, testable classes. I refactored it to KMP as step 0 of the whole migration, before touching a single feature module.</p><p>That ordering mattered more than it looks. Because the UI logic doesn’t sit inside an Android ViewModel, there was no androidx.lifecycle dependency threaded through 38 feature modules waiting to be unpicked. The state holders were already just Kotlin.</p><p><strong>Android platform code in the UI layer was already a code smell.</strong> I’d treated it that way for years, so there wasn’t much of it to find. There were some LocalContext.current calls that were easy to abstract away, and lots of text and drawable resources.</p><p>The result is visible in the module layout. Of all the feature modules, most have exactly one file outside commonMain, and it’s a preview file. Only two have anything resembling real platform code: one needs a PlatformWebView, and another needs to know whether notification settings are a thing on the actual platform at all.</p><p>If you want a single predictor of how painful your own UI migration will be, it’s probably this: how much Android your presentation layer touches that isn’t Compose itself.</p><h3>What’s actually in androidMain</h3><p><a href="https://proandroiddev.com/migrating-a-real-android-app-to-kotlin-multiplatform-1-4-before-the-migration-8b1d16f68747">Part 1</a> put androidMain at 12% of the Kotlin code, and I quoted that figure for a while as “the Android-specific part of the app”.</p><p>It isn’t. Of 13,843 lines in androidMain, <strong>9,653 lines — 70% — are </strong><strong>@Preview functions.</strong> Seventy-four files whose entire job is to render a composable in Android Studio’s preview pane and feed the screenshot tests.</p><p>Take those out and the genuinely platform-specific Android code is about 4,190 lines, under 4% of the codebase.</p><p>That reframes the whole migration for me. The thing I’d been calling “platform code” was mostly tooling scaffolding — the price of being able to <em>look</em> at the UI on one platform, not the price of <em>running</em> it on three.</p><p>That the previews sit in androidMain at all is a fossil of an abandoned attempt. The @ComponentPreview annotation is expect/actual, so the preview functions could live in commonMain — but they were written for Android’s @Preview, because the original plan was to drive the screenshot tests from them with Compose Preview Screenshot Testing. That plan didn’t survive the migration, and the previews stayed where they were.</p><p>Roborazzi wasn’t the first choice. Compose Preview Screenshot Testing is the more appealing option on paper: you write the previews you’d write anyway, and the tooling renders them. I tried it, and it doesn’t work in a Kotlin Multiplatform module. So I went back to Roborazzi, which runs under Robolectric in the androidHostTest source set the KMP Android target already provides.</p><p>The previews I’d written for the first approach are still there — 9,653 lines of them — now feeding Android Studio’s preview pane rather than a test runner.</p><h3>What didn’t move: the UI layer’s real platform seams</h3><p>Strip out the previews and what’s left is a short, specific list. All of it lives in core:designsystem and core:ui, behind expect/actual:</p><ul><li><strong>BackHandler</strong> — a back gesture on Android, a different idea on iOS, and the browser’s history on the web.</li><li><strong>FullScreenDialog</strong> — what “full screen” means is decided by the host.</li><li><strong>ContainerSize</strong> — how the available window is measured.</li><li><strong>PhotoPager and </strong><strong>ZoomAsyncImage</strong> — pinch-zoom and paging over photos.</li><li><strong>PhotoHandler</strong> and a couple of Modifier extensions in core:ui.</li><li><strong>PlatformWebView</strong> in one feature module, because an embedded web view is four different things.</li></ul><p>Everything that leaves the app entirely — a browser, the store listing, the OS notification settings — goes through a single ExternalNavigator interface with one implementation per platform, so no feature module ever touches it.</p><p>And then there’s the preview annotation itself:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3f68a9be3ce3be24510ac4cd08cd6aed/href">https://medium.com/media/3f68a9be3ce3be24510ac4cd08cd6aed/href</a></iframe><p>An expect/actual pair whose only job is to let one platform’s tooling see the code, and to make that annotation harmless everywhere else. It’s a small thing, and it’s the clearest single illustration of what this article is about: the UI moved, the tooling around it needed an adapter.</p><h3>Resources: one module, two workarounds</h3><p>All resources — strings, drawables, fonts — live in a single core:res module under commonMain/composeResources. One module, one generated Res class, every other module depends on it.</p><p>The upside of centralising is obvious: one place to look, no duplicate keys, no per-module resource packaging to configure. The cost is equally obvious — core:res is a dependency of almost everything, so touching a string rebuilds a lot.</p><p>What I didn’t expect was needing a reflective workaround to make it usable downstream. The Compose Multiplatform resources plugin doesn’t wire up the output directory of its Android assets copy task in a library module, and the task type is internal — so there’s no typed API to set it with:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/74722e310c448c05fe052d33f884e46c/href">https://medium.com/media/74722e310c448c05fe052d33f884e46c/href</a></iframe><p>Reflection in a build file is not a thing I enjoy looking at. It’s there because the alternative was no stable location for the generated assets, and the next section depended on having one.</p><h3>The safety net: Roborazzi in a KMP module</h3><p>BikeHub’s visual regression safety net is Roborazzi, running under Robolectric. The tests live in each module’s androidHostTest source set — the host-side test variant of the KMP Android library target — and one convention plugin, bikehub.multiplatform.screenshot, turns them on. Forty modules apply it. The tests sit next to the code they cover, inside the multiplatform modules themselves.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4409920f71bc3726a85fd707dc2ad696/href">https://medium.com/media/4409920f71bc3726a85fd707dc2ad696/href</a></iframe><p>A test is about as plain as it gets:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6b3af255b337aa235b0f1e56ed274b05/href">https://medium.com/media/6b3af255b337aa235b0f1e56ed274b05/href</a></iframe><p>A shared ScreenshotTest base class in core:testing handles the rest: each case renders in light and dark, across a device set defined once as specs rather than device names, captured at half scale to keep the PNGs down, compared with a 0.1 threshold.</p><h4>And this is where the resource problem came back</h4><p>Robolectric renders the UI on the JVM, and it reads resources from the module’s test assets. Compose Multiplatform resources don’t arrive there on their own, so the screenshots came out with missing images and unresolved strings.</p><p>The fix is the second half of that convention plugin: a Sync task that copies the generated resources out of core:res’s build output — the stable location the reflective workaround above created — into the host test assets, sequenced around the Android asset merge:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f6fe102e5625222cb4aa081c2289b544/href">https://medium.com/media/f6fe102e5625222cb4aa081c2289b544/href</a></iframe><p>Two workarounds chained together, one in core:res and one in the screenshot plugin, to make a Compose Multiplatform module’s resources visible to an Android host test. The application code involved in this is zero lines. Tooling again.</p><h4>What it’s worth, and what it isn’t</h4><p>Two trade-offs I’d want you to weigh before copying this setup.</p><p><strong>It only covers one platform.</strong> Compose Multiplatform renders through Skia on iOS and the web. A green screenshot suite tells me the composables are stable on Android, and nothing about the other two targets. By the way, If you do, how do you cover that gap? Any solutions are welcome :)</p><p><strong>55 MB of PNGs live in the repository.</strong> That’s the price of a diff you can look at. Recording at half scale keeps it from being much worse. CI runs verifyRoborazziAndroidHostTest, and on a pull request it re-records and pushes a commit when verification fails, which keeps the loop moving but means an unintended visual change can be accepted by not looking at it carefully enough.</p><h3>What I’d take away</h3><p><strong>Check your tooling before you change your module types.</strong> Swapping com.android.library for com.android.kotlin.multiplatform.library is a one-line change with a long tail. Anything keyed to “this is an Android library module” — tests, previews, resource packaging — needs a plan afterwards, and planning it is cheaper than discovering it.</p><p><strong>Keep platform code out of the presentation layer, for reasons that have nothing to do with KMP.</strong> It’s good practice on one platform too. It just happens to decide whether your UI layer moves in an afternoon or a month.</p><h3>Up next</h3><p><strong>Part 4: Shipping WebAssembly to Production.</strong> Getting a multiplatform app to build is one thing. Making it work reliably in a browser is another: caching, a large bundle, deep links, and the browser’s security model.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f74a6bf408ae" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/migrating-a-real-android-app-to-kotlin-multiplatform-3-4-sharing-the-ui-without-losing-safety-net-f74a6bf408ae">Migrating a Real Android App to Kotlin Multiplatform 3/4 — Sharing the UI without losing safety net</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI by lazy: R8 Shrinking and Obfuscation Handled by Your Agent]]></title>
            <link>https://proandroiddev.com/ai-by-lazy-r8-shrinking-and-obfuscation-handled-by-your-agent-e7cb3e124520?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/e7cb3e124520</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[android-app-development]]></category>
            <dc:creator><![CDATA[Eury Pérez Beltré]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 14:51:30 GMT</pubDate>
            <atom:updated>2026-11-09T14:51:29.444Z</atom:updated>
            <content:encoded><![CDATA[<h4>I gave R8 to an agent three times. The work was delegable. The evidence wasn&#39;t.</h4><figure><img alt="Illustration of a Gradle code block with isMinifyEnabled = false and a taped paper sign over it that reads DO NOT TOUCH, see Confluence." src="https://cdn-images-1.medium.com/max/1024/1*g_iHMQKCoIm9acSN5OUk4g.png" /></figure><p>Hello! 👋 Welcome to the first episode of <strong>AI by lazy</strong>, a series about the Android chores we keep postponing, handed to an AI agent and then checked by a human who is slightly less lazy than the title suggests.</p><p>We start with a classic. Open the build.gradle.kts of a mature Android app and there is a decent chance you&#39;ll find this in the release block:</p><pre>isMinifyEnabled = false</pre><p>Nobody on the team remembers why. Somebody tried once, something broke, and the flag has been guarding the release build ever since, like a “do not touch” sign that outlived the person who wrote it.</p><p>Turning R8 on is one line. Making it safe is the chore. So I gave that chore to an agent three times, with different context and different tools, and checked everything it did on a real device.</p><p>Here is the route:</p><ol><li>Why R8 stays off, and what that costs</li><li>R8 in one minute, and why reflection breaks</li><li>How I delegated it</li><li>What each run did</li><li>What I checked, and what I found</li><li>Which keep rule is right, and why this result doesn’t generalize</li><li>The rule to take away, and the verdict</li></ol><p>One disclosure before we start. The demo app, <strong>Later</strong>, was generated with AI for this series, and its technical debt was planted on purpose. Later is a to-do app for procrastinators: every task has a “Later” button that pushes it to tomorrow and increments a counter.</p><p>Yes, a series about postponed chores runs on an app about postponing. I couldn’t resist. 😅 The results below are real and reproducible on that app, but its debt is cleaner than the debt in a product with years of history, and I’ll come back to that.</p><h3>Why R8 stays off</h3><p>At a previous job, the team had tried to enable R8, ran into problems, wrote it down in Confluence and moved on. A year later I joined that company, enabled it by hand, with no AI to help, and it went fine.</p><p>I don’t tell that story to look clever. Whatever broke the first time was real at that moment. What interests me is what happened next: a failure nobody re-tested kept R8 off for a year. The Confluence page became the truth, and nobody asked what we actually knew when we said “R8 breaks the app.”</p><p>Keep that question in mind, because it comes back at the end with a twist.</p><p>Leaving R8 off isn’t free. You ship more code than you use, startup pays for it, and Google has started to measure it: Android vitals now flags apps whose DEX is poorly optimized, and apps with more than 10 MB of DEX that don’t meet its threshold “may see store visibility impact starting from February 2027” (<a href="https://developer.android.com/google/play/vitals">Android vitals</a>).</p><p>Google also publishes partner stories with big numbers, like <a href="https://developer.android.com/blog/posts/how-reddit-used-the-r8-optimizer-for-high-impact-performance-improvements">Reddit reporting 40 % faster cold starts</a> after enabling the full power of R8. Those are self-reported case studies, not an average you should expect, but they explain why “we’ll do it later” is getting more expensive.</p><h3>R8 in one minute</h3><p>R8 is the optimizer that runs on your release build. It does three jobs:</p><ul><li><strong>Shrinking:</strong> it starts from your entry points (activities, services, anything the manifest declares) and removes the code nothing reaches.</li><li><strong>Optimization:</strong> it rewrites what’s left, inlining methods and merging classes.</li><li><strong>Obfuscation:</strong> it renames classes, fields and methods to short names like a and b, which also makes the DEX smaller.</li></ul><p>Since AGP 8.0, R8 runs in full mode by default, which is more aggressive: for example, keeping a class no longer implicitly keeps its no-args constructor. And since AGP 9.3 there is a simpler way to turn it all on:</p><pre>buildTypes {<br>    release {<br>        optimization {<br>            enable = true<br>        }<br>    }<br>}</pre><p>That one block enables code shrinking, resource shrinking and a set of default keep rules equivalent to proguard-android-optimize.txt, and your own keep rules live in .keep files under src/main/keepRules/ (<a href="https://developer.android.com/topic/performance/app-optimization/enable-app-optimization">Enable app optimization</a>).</p><h4>Where reflection breaks</h4><p>Now the part that bites. R8 decides what to keep by following references in your code. Reflection hides those references. Later downloads its “remote” tasks as JSON and parses them with Gson into two small Kotlin data classes, RemoteTask and RemoteTaskResponse. Nothing in the app ever calls their constructors; Gson creates them at runtime. From R8&#39;s point of view, those are classes nobody builds and fields nobody reads.</p><p>To see the failure for ourselves, we took the starting point of the experiment, turned R8 on with that block and no keep rules, and tapped sync on a Pixel 7. The APK went from 11.5 MB to 4.2 MB, and the first sync crashed:</p><pre>lj0: Abstract classes can&#39;t be instantiated! Adjust the R8 configuration or register<br>an InstanceCreator or a TypeAdapter for this type. Class name: ke1</pre><p>R8 had removed the constructor and marked the class abstract, and ke1 is what was left of its name. Even the exception got renamed: lj0 is Gson&#39;s JsonIOException in disguise, which is why release crash reports need the mapping file to make sense. This time we actually watched it fail. Remember that, too.</p><h3>How I delegated it</h3><p>The setup:</p><ul><li><strong>Agent:</strong> Claude Code with Claude Opus 5.5.</li><li><strong>Tools available in the repo:</strong> Google’s official <a href="https://github.com/android/skills">r8-analyzer</a> skill, installed with the <a href="https://developer.android.com/tools/agents/android-cli">Android CLI</a> 1.1, and my own <a href="https://github.com/euri16/android-ai-skills">android-emulator-control</a> skill, which builds, installs and drives an app on a device.</li><li><strong>Device:</strong> a Pixel 7 on Android 17, connected over USB.</li><li><strong>Project:</strong> Later on AGP 9.5.1 and Gson 2.15.0, starting from the tag ep1-before.</li></ul><p>These results belong to this model and these versions. A different agent, or this one next month, may behave differently. What should transfer is the way we checked.</p><p>I ran the same chore three times, each in a fresh session and on its own branch:</p><p>Run A got the blind prompt with no skills visible (they were installed after the session started). Run A&#39; got the same blind prompt with the skills visible. Run B got the prompt with context about Gson, skills visible.</p><p>The blind prompt:</p><blockquote><em>Enable R8 for the release build of this app and make it production-ready. Done means the release build works on my Pixel 7, which is connected over USB. Don’t change anything unrelated to R8.</em></blockquote><p>Run B added one sentence: “The app parses JSON from RemoteTaskSource with Gson, so check everything that relies on reflection.&quot;</p><p>The sentence I care about most is the one in the middle. “Production-ready” is the “let’s circle back” of prompts: everybody nods, nobody can verify it. “Done means the release build works on my Pixel 7” is something a device can prove. If the agent’s idea of done is a green build, R8 will happily let it ship the crash we just saw.</p><h3>What each run did</h3><p>The good news first, because it’s genuinely good. All three runs found the Gson problem on their own, even the blind ones. All three wrote rules for those two classes only, instead of keeping the whole package. All three installed the release build on the phone and ran the sync, the code path that crashed.</p><p>And two of them noticed that the release build was still signed with the debug key, flagged it as something Play would need fixed, and left it alone because it wasn’t R8.</p><figure><img alt="Later running as a release build on a Pixel 7, with a snackbar that reads 20 tasks synced." src="https://cdn-images-1.medium.com/max/1024/1*jNeEnMqa4pXEP1smeOHWkw.png" /><figcaption>The ep1-after release build on the Pixel 7, right after tapping sync.</figcaption></figure><figure><img alt="Comparison of the three runs: how R8 was enabled, keep rules written, whether r8-analyzer ran, whether the crash was seen before fixing, DEX size and obfuscation." src="https://cdn-images-1.medium.com/max/1024/1*jMHYSVXZXlXbdedNAYT78w.png" /><figcaption>The three runs side by side.</figcaption></figure><p>For reference, the release build before R8 had 23.14 MB of uncompressed DEX and about 1 % obfuscation. Both rows come from android analyze, whose obfuscation figure is a heuristic. Run B also ran R8&#39;s Configuration Analyzer, which scores things its own way: 98.77 % optimization, 99.22 % obfuscation and 99.16 % shrinking.</p><h4>What differed between runs</h4><p>Run A used the older DSL. It works, the same way a flip phone still makes calls, but it suggests the agent was working from general knowledge rather than the current docs.</p><p>Run A’, with the skills visible, learned the AGP 9.3 DSL from the references inside r8-analyzer, then decided not to run the analyzer itself: it reasoned that the skill audits existing rules and the job was to enable R8.</p><p>Run B, the one with context, did the most complete job: it used the annotation the Gson docs recommend, ran the full analyzer, and even installed a debug build over the release one to read the synced rows straight from the database before reinstalling the release build.</p><p>On paper, three successes. So let’s do the part this series is named after.</p><h3>What I checked, and what I found</h3><h4><strong>None of the three saw the crash</strong></h4><p>Every run read the code, spotted Gson and wrote the rule before its first R8 build. They were right. But they were right by inference, and one of them said so with more honesty than most humans manage:</p><blockquote><em>“That the rule was needed is an inference, not something I saw.”</em></blockquote><p>Here’s the twist I promised. The Confluence page was a failure nobody re-tested. These runs produced a fix nobody saw fail. One belief keeps R8 off; the other declares it done. Neither is evidence.</p><h4><strong>Confident claims that weren&#39;t true</strong></h4><p>When I asked each run to walk me through what it did, the corrections started:</p><ul><li>Run A’ reported that the sync added 12 tasks. The JSON has 20, and the app said 20. The 12 came from a head -12 it had used to print some titles, and that&#39;s how head -12 gets promoted to a fact. It also admitted it never saw the &quot;synced&quot; message itself; it only checked that one category count went up by four.</li><li>Run B reported that it had opened a task detail screen with no crashes. When asked, it said it couldn’t back that up: the tap may not have opened anything.</li><li>Run B also left this comment in proguard-rules.pro, stated as fact: without a rule keeping the constructors, &quot;R8 full mode treats them as never instantiated and strips the field reads.&quot; It never tested that. We did: same configuration, rule removed, built and installed on the Pixel 7. Sync worked, and when we read the database afterwards every synced title, note and category was intact. The comment was wrong for this setup, and it would have lived in the repo as documentation.</li></ul><p>None of these are dramatic. That’s the point. They are the small, plausible, unchecked sentences that end up in a pull request description, and later in somebody’s Confluence page.</p><h4><strong>Skills used as documentation, not tools</strong></h4><p>Run A never saw the skills. Run A’ read them and skipped the parts that measure. Only run B ran the analyzer, and only after it had already decided on its rules. Having a tool in the repo doesn’t mean the agent will use it to check its own work.</p><h4><strong>Driving the phone</strong></h4><p>This is where things got entertaining. Later’s sync button has no contentDescription, so it doesn&#39;t show up in a UI dump in any useful way. Every run ended up tapping raw screen coordinates for at least part of the app.</p><p>Run A&#39;, in a hurry, fired a burst of taps with no waits; a BACK press took it out of the app, the remaining taps landed on the launcher and opened Google&#39;s voice search, and it force-stopped the Google app to recover. Nobody asked it to touch the Google app. 🙃 (That missing contentDescription was planted for the accessibility episode. It showed up early.)</p><h3>Which keep rule is right</h3><p>All three configurations work on Later, and the one I kept as ep1-after goes one step further than any of them: optimization { enable = true }, @SerializedName on every field, a default value on every constructor parameter, and no hand-written rule at all.</p><h4>Why the default values</h4><p>Run B’s instinct was right: these classes need a constructor. It just kept the wrong one. Gson creates objects through a no-args constructor, and these data classes didn’t have one.</p><p>Without it, Gson falls back to Unsafe, which allocates the object without running any constructor, so Kotlin default values and init blocks never run. On Later every field still arrived intact, but that&#39;s a fallback, not a design. The <a href="https://github.com/google/gson/blob/main/Troubleshooting.md">Gson troubleshooting guide</a> asks for exactly this: a no-args constructor that R8 keeps.</p><p>Give every parameter a default and Kotlin generates that constructor; Gson&#39;s bundled rules (Gson 2.11 and later) keep it, together with the fields annotated with @SerializedName. We checked the mapping file and the device: the no-args constructor survived, and the sync works. That last change was a human one.</p><p>I’d ship that version, and in a code review I’d ask for it. Not for the few kilobytes. A -keep class rule names the class by its full path, so the day someone moves that model to another package, the rule stops applying. The build won&#39;t complain; only R8&#39;s Configuration Analyzer lists it as an unused rule, if anyone reads the report. Until then, the crash is back, only in release. The annotation and the defaults travel with the code.</p><h4>Why this result doesn&#39;t generalize</h4><p>Why did reading the code work so well here? Because in Later, all the reflection lives in our own code. Grep for Gson, find two classes, done. Real apps are rarely this polite.</p><p>On a project I’m working on right now, R8 has to be enabled again. A previous attempt failed, nobody recorded what happened, and the trouble apparently came from third-party libraries, not from the build. That’s the harder case: the reflection hides in dependencies nobody reads, so an agent that infers from your code will find nothing to infer. Only running the release build and watching it fail will tell you. It’s also the Confluence story again, minus the Confluence page.</p><h4>The longer-term answer</h4><p>Straight from the source: Gson’s own README says it “is not a recommended library for interacting with JSON on Android.” Libraries that generate code instead of using reflection, like kotlinx.serialization or Moshi with codegen, avoid this whole category of R8 surprises. That’s a migration, not “turn on R8”, so none of the runs did it, correctly.</p><h3>The rule to take away</h3><p>If I could add one paragraph to that old Confluence page, it would be the same one I now add to the prompt. Three lines that would have saved that team a year of isMinifyEnabled = false:</p><ol><li><strong>Make it fail first.</strong> Build the release without new rules, reproduce the crash, then fix it. A rule that nobody saw fail is a guess, even when it’s a good guess.</li><li><strong>Separate what you verified from what you inferred.</strong> Ask the agent to label both in its summary, and treat any sentence without evidence as a TODO, not a fact. That includes comments it writes in your code.</li><li><strong>Define done as something a device can prove.</strong> “Works on my Pixel 7” gave every run the right target. “Production-ready” alone would have been satisfied by a green build.</li></ol><figure><img alt="Flowchart of six steps: enable R8, build release, run on a device, watch it fail, fix narrowly, audit the rules, then back to build release. Watch it fail is highlighted." src="https://cdn-images-1.medium.com/max/1024/1*diRD51O_T54icmWDk9z2lg.png" /><figcaption>The loop the agent should close. Every run skipped the orange step.</figcaption></figure><h4>The verdict for this episode</h4><figure><img alt="Verdict table with three columns: delegate it, supervise it, do not delegate it." src="https://cdn-images-1.medium.com/max/1024/1*cgXEPy4nIaBEcNI_1yGZTQ.png" /><figcaption>The verdict for this episode.</figcaption></figure><h3>Wrapping up</h3><p>The agent did the chore, and did it well: a release build a fraction of the size, narrow rules, no package-wide keeps, and good instincts about what was out of scope. What it couldn’t give me, until I asked, was evidence. The real test is a codebase like the one I described, where the reflection belongs to someone else’s library.</p><p>If you want to try this yourself, the repo is at <a href="https://github.com/euri16/later">github.com/euri16/later</a>. Check out the ep1/setup branch (the starting point plus both skills), give your agent the same prompt, and compare its result with the ep1-after tag. Next episode, with R8 finally on, we profile Later&#39;s jank, because performance numbers on an unminified build don&#39;t mean much.</p><p>Thanks for reading! 🙌 What’s the weirdest crash you’ve only ever seen in a release build? Tell me in the comments. Bonus points if it was documented somewhere nobody reads.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e7cb3e124520" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/ai-by-lazy-r8-shrinking-and-obfuscation-handled-by-your-agent-e7cb3e124520">AI by lazy: R8 Shrinking and Obfuscation Handled by Your Agent</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Compose Multiplatform: Why You Shouldn’t Build Your Whole iOS App in Compose]]></title>
            <link>https://proandroiddev.com/compose-multiplatform-why-you-shouldnt-build-your-whole-ios-app-in-compose-9e1e8c978a5b?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/9e1e8c978a5b</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[android-app-development]]></category>
            <dc:creator><![CDATA[Anatolii Frolov]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 00:22:32 GMT</pubDate>
            <atom:updated>2026-11-09T00:22:30.800Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to keep your screens shared while letting iOS handle navigation and system controls.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZZdAkIasBVd3LmYGT5tNeQ.png" /></figure><p>Compose Multiplatform lets Android developers reuse Kotlin and Compose screen code on iOS. You keep the language and the UI toolkit you already know, and the same screens run on an iPhone.</p><p>But a screen that runs on iOS does not automatically follow iOS. You can build the whole app in Compose, including the tab bar and navigation, and Compose Multiplatform supports the back gesture and iOS-style transitions. What it cannot do is redraw your own tab bar when Apple redesigns the system one. A Compose copy of a system control stays the way you drew it.</p><p>I faced this choice while building <a href="https://clarimeal.com/?ct=medium-kmp-share-the-product">ClariMeal</a>, a calorie tracker for iOS and Android. I wanted the product screens shared, and I wanted the navigation and system controls to come from iOS itself. The reason was maintenance. Liquid Glass in iOS 26 is a system material, and Compose cannot draw it. On Android, ClariMeal has its own glass tab bar built in Compose, and it looks good there. On iOS it would still be an imitation: it looks different and does not behave like the system bar. Only the system bar follows iOS when Apple changes it.</p><p>That did not require a SwiftUI rewrite. Screen content stayed in Compose and the logic stayed in Kotlin, while native iOS components took over navigation and system interactions. This article explains which parts of your iOS app are better left to the system when you want them to follow iOS, where I drew that boundary, and what it still cost.</p><h3>1. The tab bar belongs to the operating system</h3><p>Start with one comparison: the same tab bar on Android, iOS 18 and iOS 26.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C5Po3iEri5DBbagktvGXcw.png" /></figure><p>On Android, Compose draws the whole bar, including the + button in the middle. On iOS, the tab bar comes from UIKit, and the + button shows where the line between the system and the app runs.</p><p>The app supports iOS 17 and later. Before iOS 26, the + button is a circle in the middle of the system tab bar, and that circle is my code. On iOS 26, I repurpose UISearchTab for Add. The app replaces its search icon with a plus and intercepts taps to open the Add flow without selecting the tab. UIKit renders the detached circle at the trailing edge of the tab bar.</p><p>These excerpts omit the setup of the navigation tabs and the placeholder controller:</p><pre>if #available(iOS 26.0, *) {<br>    let addTab = UISearchTab { _ in UIViewController() }<br>    addTab.automaticallyActivatesSearch = false<br>    addTab.image = UIImage(systemName: &quot;plus&quot;)<br>    addTab.title = runtime.addControlTitle()<br>    uiTabs.append(addTab)<br>    tabs = uiTabs<br>} else {<br>    // iOS 17-25: a placeholder item in the middle,<br>    // and we draw the circle ourselves<br>    controllers.insert(placeholder, at: Self.centerAddIndex)<br>    viewControllers = controllers<br>}</pre><p>The slot is never selected as a destination. The tab bar delegate turns the tap into the Add flow:</p><pre>@available(iOS 18.0, *)<br>func tabBarController(<br>    _ tabBarController: UITabBarController,<br>    shouldSelectTab tab: UITab<br>) -&gt; Bool {<br>    if tab is UISearchTab {<br>        runtime.presentAddFlow()<br>        return false<br>    }<br>    return true<br>}</pre><p>That is the whole button on iOS 26. None of this code draws it: UIKit places the circle at the edge of the tab bar, adds the glass and sets the shape. My code only tells UIKit that this tab is Add and what a tap should do.</p><p>Apple explains that UIKit apps adopt the new appearance when they are rebuilt with the iOS 26 SDK. Native controls remove the need to reproduce that appearance yourself. They do not remove SDK updates, integration work or testing.</p><p>The same rule applies to the navigation bar, swipe back and sheet presentation. On iOS 26, the app also uses the system scroll edge effect under the navigation bar instead of drawing its own blur. These are the parts of the interface where iOS should decide how iOS behaves.</p><h3>2. The calorie card belongs to the product</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BQUxiGZuHDEcKP6EIBjWrA.png" /></figure><p>This is the card at the top of the Today screen. It shows the calories left for the day, a ring and three bars for protein, carbs and fat.</p><p>The number, the ring, the colors of the macros and the spacing between them are product decisions. On Android, the same Compose code draws this card. There is no reason to redesign it because Apple redraws a tab bar.</p><p>The content of the Today, Progress and Summary screens stays in Compose. Food cards, insight cards, charts, macro rows and scan results share one implementation as well. The business logic, the Room KMP database and the insight engine also stay in Kotlin.</p><p>Android is a different case. There, Compose is the platform’s own UI toolkit, so the screens, the navigation and the bottom bar all use Compose. On iOS, the system controls come from UIKit, and that is where the boundary matters.</p><p>For me, iOS-first describes the experience. It does not require every screen to be written in SwiftUI.</p><h3>3. Native navigation can host shared screens</h3><p>The main tab interface uses ShellCoordinator, a Swift UITabBarController subclass. It contains four navigation tabs, each with its own UINavigationController, plus a separate Add action.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5wNrjyoce-6J_MI0wZYzww.png" /><figcaption>On iOS, UIKit owns the header and the tab bar, and Compose draws the content between them. On Android, Compose draws everything.</figcaption></figure><p>Each screen in those stacks is a UIViewController that hosts one Compose screen through ComposeUIViewController. When Compose sends a navigation event, the native side pushes the next controller.</p><p>Shared navigation wrappers use AppNavigator for push and pop. Here are three methods from that interface:</p><pre>interface AppNavigator {<br>    fun push(dest: NavDest, singleTop: Boolean = false)<br>    fun pop()<br>    fun popTo(dest: NavDest, inclusive: Boolean = false)<br>}</pre><p>Each platform implements it with its own stack. On Android, Compose Navigation owns it. On iOS, a UINavigationController does. These method excerpts show how the two implementations change their stacks:</p><pre>// Inside NavHostNavigator (Android)<br>override fun push(dest: NavDest, singleTop: Boolean) {<br>    navController.navigate(dest) { launchSingleTop = singleTop }<br>}<br><br>// Inside ShellNavigator (iOS)<br>private fun pushNew(dest: NavDest) {<br>    val nav = nav ?: return<br>    if (!buildAllowed(nav)) return<br>    nav.pushViewController(makeHosted(dest), animated = true)<br>}</pre><p>makeHosted wraps the same Compose screen that Android shows into a ComposeUIViewController. Callers use AppNavigator without choosing a platform stack.</p><p>This makes the ownership clear. Compose renders the content of the destination. UIKit owns the stack, the navigation bar and the back gesture.</p><p>In this app, UIKit owns every destination stack, onboarding included. For destinations owned by UIKit, avoid keeping a second back stack in Compose. Send navigation requests to the native layer and let it own push and pop. Otherwise the two layers can disagree about which screen is open.</p><p>Swift hosts the containers and coordinates presentation and dismissal. The Add search flow uses another UITabBarController, created in Kotlin and presented in a sheet. Native UI does not require every controller to be created in Swift.</p><p>Food search uses UISearchController. Birthday input uses UIDatePicker and UIPickerView. Other screens use UISlider, UISegmentedControl, UIAlertController and UISheetPresentationController.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9515l8DvWMNrW1mVKkb3CA.png" /></figure><p>JetBrains shows this approach in a tutorial that moves navigation to SwiftUI’s TabView and NavigationStack while Compose keeps rendering screen content. My implementation uses UIKit for the same split. It is one available approach, not a requirement of Compose Multiplatform. The UIKit integration documentation also shows Compose content inside a UITabBarController with UINavigationController wrappers.</p><h3>4. A copied system detail is still your code</h3><p>Some Compose details deliberately match iOS. Row heights, separators, margins and the card radius were measured from iOS 26 system screens such as Settings and Health. The sampled cards had a 26 pt corner radius. These measurements became design tokens in the Compose theme.</p><p>A token makes a value easy to change. It does not make it native. Apple cannot update a number stored in your Compose theme.</p><p>The calorie card combines product-specific content and layout with these shared tokens, including the radius copied from iOS. Its implementation stays shared, but the copied details remain my responsibility. When iOS changes the details I chose to match, I review those tokens: some need updating, others can stay as part of the product’s design.</p><p>This is where I draw the boundary. The system renders the controls whose look and behavior it should own. Shared components carry the product design. Any system detail copied into them stays your responsibility.</p><h3>5. A full SwiftUI rewrite would move the boundary too far</h3><p>For this product, rewriting the screen content in SwiftUI would mean building each screen twice. A change to the calorie card or a chart would then need the same change on Android and on iOS.</p><p>I did not see enough value in that. Native navigation and controls cover the platform interactions I wanted iOS to own, and Compose keeps the product content shared. The business logic would stay in Kotlin either way.</p><h3>What to remember</h3><ul><li>Let native iOS containers own the navigation you want iOS to define.</li><li>Keep product screen content shared when it serves both platforms.</li><li>Send navigation events from Compose to the native stack.</li><li>Treat copied system measurements as design tokens that you re-check with each SDK.</li></ul><p>The rule I use: share the product. Don’t share the operating system.</p><p>You can see this setup in <a href="https://clarimeal.com/?ct=medium-kmp-share-the-product">ClariMeal</a>.</p><p>If this helped you decide where to separate shared and native UI, a clap helps other developers find the article.</p><h4>You might also like:</h4><ul><li><a href="https://proandroiddev.com/kotlin-coroutines-the-real-difference-between-job-cancel-and-scope-cancel-05e1d9dd5245">Kotlin Coroutines: The real difference between Job.cancel() and Scope.cancel()</a></li><li><a href="https://proandroiddev.com/when-kotlin-object-stops-being-a-singleton-604a3354e4ec">When Kotlin object Stops Being a Singleton</a></li><li><a href="https://levelup.gitconnected.com/5-jetpack-compose-tricks-that-saved-me-hours-in-real-projects-32d43606e5f0">5 Jetpack Compose Tricks That Saved Me Hours in Real Projects</a></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*A4KalWbkquFloyMFliGwCA.png" /></figure><p><strong>Anatolii Frolov</strong><br>Senior Android Developer<br>Writing honest, real-world Kotlin &amp; Jetpack Compose insights.<br>📬<a href="https://medium.com/@af2905g"> Follow me on Medium</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9e1e8c978a5b" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/compose-multiplatform-why-you-shouldnt-build-your-whole-ios-app-in-compose-9e1e8c978a5b">Compose Multiplatform: Why You Shouldn’t Build Your Whole iOS App in Compose</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Android Dev Summit 2026]]></title>
            <link>https://proandroiddev.com/android-dev-summit-2026-6b6cdf59cbe0?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/6b6cdf59cbe0</guid>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[android-dev-summit]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Jonathon Reid]]></dc:creator>
            <pubDate>Sun, 08 Nov 2026 14:47:09 GMT</pubDate>
            <atom:updated>2026-11-08T14:47:07.850Z</atom:updated>
            <content:encoded><![CDATA[<h4>Insights on XR, Android CLI, Compose and everything in between</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sj1HaVggLvO01DjDGryVbQ.jpeg" /><figcaption>Source: Photo by <a href="https://medium.com/u/a74d2aac76a8">Jonathon Reid</a></figcaption></figure><h3>Overview</h3><p>Attending the Google Android Dev Summit in Mountain View was an exciting experience given it was my first time attending an official event at Google HQ. Compared to massive, spectacle-rich conferences like Google I/O, and Cloud Next, the scale here was noticeably more intimate, allowing direct technical conversations with Google’s core platform team and offering unique insight into shifting engineering priorities at Google — more on that to come.</p><p>The primary focus centered on enabling agentic coding paradigms, by introducing BYO model capabilities in Android Studio, and expanding command-line tooling via the Android CLI, while also enabling richer and more performant user interfaces with Compose/A2UI improvements.</p><p>Being there in person was insightful as it provided additional context on upcoming architectural and cultural shifts in the Android ecosystem, revealed by what was said, but more importantly, by what was left unsaid. This was particularly evident with Android Studio: while it received a minor update, it clearly took a back seat to the Android CLI updates and seems to be falling out of favor with Google as the preferred developer experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BjVq_nPgXyAXkp0IxnNZ8g.png" /><figcaption>Google leaning heavily into adding agentic support. Source: <a href="https://www.youtube.com/watch?v=w-auFZP5gJo">Android Developers Youtube</a></figcaption></figure><h3>Android CLI &amp; Agentic Workflows</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BGvMGy0Wj9NYsjzJ47gmEw.jpeg" /><figcaption>The agentic development roundtable to wrap up ADS. Source: Photo by <a href="https://medium.com/u/a74d2aac76a8">Jonathon Reid</a></figcaption></figure><p>The introduction of extended command-line interface (CLI) support for Android represents a major cultural shift towards agentic automation. Built specifically to improve execution performance and minimise context token usage, Android CLI allows autonomous AI agents to execute code development, build tasks, run tests, and confirm they have satisfied the requirements in a verification loop, with the simplicity and performance of the terminal.</p><p>A key insight emphasised during the sessions was the importance of <a href="https://youtu.be/Mb9H43ia0kE?t=720&amp;si=X0aibyWWveLoy6hW">hand-crafting your repository’s agents.md files</a> rather than delegating them entirely to AI generation, ensuring strict project standards and precise instruction guardrails. By hand-writing the documentation, AI agents are more likely to follow it as expected. This was particularly interesting to me, as it highlights the inherent risk of relying on AI to define the rules while also being the one to execute the task.</p><blockquote>Full video from Ben &amp; Steven: <a href="https://www.youtube.com/watch?v=Mb9H43ia0kE">Agentic Engineering with Android CLI</a></blockquote><p>Agentic development often struggles when agents require human intervention for constant, trivial choices. Too often, I’ve set an agent to a task, stepped away, and returned 15 minutes later only to find zero progress because it stalled on a basic naming decision. <a href="https://www.youtube.com/watch?v=Mb9H43ia0kE&amp;t=638s">A proven alternative in the agentic SDLC is the ‘grill me’ approach:</a> where you prompt the AI to ask all clarifying questions upfront, then execute the task autonomously. Paired with self-verification loops, this eliminates constant hand-holding and enables material productivity gains.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*04RPeGpLhN7va8UkUIvMBg.jpeg" /><figcaption>Example agentic workflow — starting with /grill-me. Source: Photo by <a href="https://medium.com/u/a74d2aac76a8">Jonathon Reid</a></figcaption></figure><p>Another insight was that <a href="https://youtu.be/Mb9H43ia0kE?si=EYAUbhSFatp7u9LN&amp;t=1573">agents are often far too confident in their own abilities and finish a task thinking it is complete when it’s really not</a>. Just think how many times have you received an answer from an AI, asked it to double-check its work, only to watch it immediately backpedal.</p><p>From Google’s experience, asking the agent to verify and show you the requirements are met, materially increases the reliability of the outcome. This inherently increases the runtime demanded given the longer verification loop, but generally provides better overall results as there’s less re-work required.</p><p>To summarise, by resolving any ambiguities early on in the SDLC workflow, it allows us, as experienced engineers, to focus on other, more nuanced items while the agent is running in the background doing what it does best. This is where we can add real value as humans and provide a differentiating factor to agents.</p><blockquote>To explore these developments further, check out the official presentation and developer documentation:</blockquote><blockquote>Full Presentation: <a href="https://www.youtube.com/watch?v=Mb9H43ia0kE">Agentic Engineering &amp; Android CLI</a></blockquote><blockquote>Developer Guide: <a href="https://developer.android.com/tools/agents/android-cli">Android CLI Tooling Documentation</a></blockquote><h3>Standardising Best Practices with Android Skills</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IrwzEtsB-9XGbjrSoXYhCQ.png" /><figcaption>How to view and install new Android CLI skills — Source: <a href="https://youtu.be/Mb9H43ia0kE?si=XVw1Rt9nqdQUVJ_7">Android Developers Youtube</a></figcaption></figure><p><a href="https://www.youtube.com/live/8Lmv7LGgL4E?si=gMUdmfka1BGvT3qO&amp;t=494">AI agents often fail to adhere to Modern Android Development (MAD) patterns,</a> frequently generating legacy boilerplate, outdated patterns, or outright hallucinating. This does align with my experience working on both Android and .NET APIs, where agents are generally much better at generating .NET API code over Android code. Though this is not surprising given the Android ecosystem evolves so quickly, models often lack the volume of training data found in more stable environments like .NET.</p><p>To solve this, at ADS, <a href="https://github.com/android/skills">Google announced over 30 official Android Skills</a> to set engineering standards for code-generation agents. These skills specifically target:</p><ul><li><strong>New functionality:</strong> Defining a knowledge base for recently released features that AI hasn’t been trained on yet.</li><li><strong>Complex implementations:</strong> Guiding hardware integrations like advanced camera APIs that are specific to Android.</li><li><strong>High-risk areas:</strong> Android Security protocols &amp; <a href="https://youtu.be/OaZ-_hTaZIA?si=k6eTUljxR0bGjN_R&amp;t=131">Play Store compliance.</a></li></ul><p>These new announcements demonstrate that Google acknowledges the current ecosystem has not been favorable to agentic engineering, and is now providing flexible, AI-friendly standards so developers can adopt what works best for them.</p><h3>Next-Generation Android Prototype Tooling</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*chfE8eWX_tGmFAR9XgzKkQ.png" /><figcaption>An example of running numerous instances of the new prototype at once. Source — <a href="https://www.youtube.com/watch?v=8Lmv7LGgL4E">Android Developers Youtube</a></figcaption></figure><p>Another standout revelation was an upcoming, not-yet-named rework of the Android Emulator that runs completely differently.</p><p>Engineered to address performance limitations, this <a href="https://www.youtube.com/live/8Lmv7LGgL4E?si=o44JRZ52om1FGAaC&amp;t=805">next-generation Android ‘simulator’ (currently just called ‘prototype’)</a> provides faster boot times, significantly lower system resource consumption, and smoother runtime execution compared to traditional Android emulators.</p><p>This leap in performance is critical for local agent development loops and high-throughput automated testing runs. And allows agents to verify their work much more efficiently.</p><p>Anyone who has been working on Android for some time knows how big a leap this is and it was one of the most talked about changes at the event. Everyone was excited to be able to run and test their apps quickly, especially if you work on enterprise sized apps — this is a game changer.</p><h3>Hands-On with Android XR &amp; Spatial Computing</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_Ij85Xm71w4WTtQb-_y8xA.jpeg" /><figcaption>XREAL announcing their new Aura Android XR headset.-Source: Photo by <a href="https://medium.com/u/a74d2aac76a8">Jonathon Reid</a></figcaption></figure><p>Hands-on workshops provided direct real-world time with emerging Android XR devices and spatial app development workflows like <a href="https://www.youtube.com/live/8Lmv7LGgL4E?si=TT9cow6OjNYHK7kI&amp;t=2671">the newly announced XREAL Aura</a>.</p><p>These workshops demonstrated how the ecosystem separates light, head-mounted glasses from an external computing ‘puck’ that handles primary processing, enabling extended screen mirroring and multi-window productivity layouts.</p><p>The current trajectory seems to focus on productivity, resting media consumption, and hands-free utility rather than continuous, all-day wear.</p><p>While I did feel a little nauseous wearing them for longer periods (though given the limited headsets available, I had the wrong size for me so that likely contributed), they were impressive in their presentation ability and hand tracking. This made the experience feel quite intuitive and became quite ‘routine’ after only a short amount of time — a good sign new tech will become normalised to people.</p><p>It still remains to be seen whether they will get large-scale user adoption, but I can see this generation of AR fulfilling a niche in the market more than the full VR headsets, or the glasses-only options which have significant privacy concerns.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TG_JRKFAkBrW-yCsmUc2MQ.jpeg" /><figcaption>Myself, <a href="https://medium.com/u/6d77f07dc67">Katie Barnett</a>, and <a href="https://medium.com/u/f1960db014b5">Kartik Arora</a> at the Aura event — XR workshop coming soon!. Source: Photo by <a href="https://medium.com/u/a74d2aac76a8">Jonathon Reid</a></figcaption></figure><h3>Conclusion &amp; Outlook</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SOPcgXnc7xnua-cZUbLrtA.jpeg" /><figcaption>Photo taken and used with permission by <a href="https://medium.com/u/6d77f07dc67">Katie Barnett</a></figcaption></figure><p>The Android Dev Summit made it clear that Android development is rapidly evolving to prioritise AI-driven, agentic development workflows. By extending support for the Android CLI, standardising Android Skills, and experimenting with more efficient virtualisation tools, Google is establishing a decisive lead in modern agent developer enablement.</p><p>A2UI is also being enhanced to provide a pattern for dynamic, agentic experiences, right in the palm of your hand. This is where companies will be able to differentiate themselves from their competitors with richer and smoother experiences. More details: <a href="https://www.youtube.com/watch?v=qIvsFGvlHdo">Agentic UI with A2UI</a></p><p>For mobile engineers and technical leaders, adopting these tools is no longer optional, but critical for supporting high-performing engineering teams, shipping standout user experiences, and remaining competitive in a rapidly evolving ecosystem.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6b6cdf59cbe0" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/android-dev-summit-2026-6b6cdf59cbe0">Android Dev Summit 2026</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Clean Up Your Conditional Logic: Exploring Kotlin 2.5.0-Beta2’s New Boolean & Nullable Utilities]]></title>
            <link>https://proandroiddev.com/clean-up-your-conditional-logic-exploring-kotlin-2-5-0-beta2s-new-boolean-nullable-utilities-7854207156ea?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/7854207156ea</guid>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[clean-code]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Nav Singh]]></dc:creator>
            <pubDate>Sat, 07 Nov 2026 16:39:54 GMT</pubDate>
            <atom:updated>2026-11-07T16:39:53.024Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m9n1bJgTuTpW7nu62k0gBQ.jpeg" /><figcaption>AI generated image</figcaption></figure><p>Ditch redundant if-else boilerplate and awkward scope chaining with the newly introduced onTrue(), onFalse(), and ifOrNull() functions.</p><p>Kotlin has always excelled at eliminating boilerplate. From concise data classes and standard scope functions (apply, also, let, takeIf) to smart casting, the language consistently seeks ways to make everyday idioms more expressive.</p><p><strong>Kotlin </strong><strong>2.5.0-Beta2 </strong>adds standard library functions that let us inspect a value before returning it, or return a nullable <strong>result</strong> based on that <strong>value</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/636/1*DimHBVzSkkMOE96OKkPWGw.png" /><figcaption><a href="https://kotlinlang.org/docs/whatsnew-eap.html?utm_campaign=kotlin-releases&amp;utm_content=2-5-0-beta2&amp;utm_medium=social&amp;utm_source=twitter#standard-library">https://kotlinlang.org/docs/whatsnew-eap.html?utm_campaign=kotlin-releases&amp;utm_content=2-5-0-beta</a>2</figcaption></figure><ul><li><strong>Boolean.onTrue(action: () -&gt; Unit): Boolean</strong></li><li><strong>Boolean.onFalse(action: () -&gt; Unit): Boolean</strong></li><li><strong>ifOrNull(condition: Boolean, action: () -&gt; T): T?</strong></li></ul><h3>Implementation</h3><ul><li>Because these functions are introduced as experimental APIs in the standard library, we need to opt in to use them.</li></ul><ol><li>File/Function Annotation<br>OptIn(ExperimentalStdlibApi::class)</li><li>Module-wide via Gradle config</li></ol><pre>kotlin {<br>    sourceSets.all {<br>        languageSettings.optIn(&quot;kotlin.ExperimentalStdlibApi&quot;)<br>    }<br>}</pre><h3>onTrue() &amp; onFalse()</h3><ul><li>Both functions invoke a supplied block when the receiver matches the <strong>condition</strong> and <strong>return</strong> the <strong>original </strong><strong>boolean unmodified</strong>.</li></ul><h4>Example</h4><pre><br>data class User(val id: String, val email: String)<br><br>class AuthService() {<br><br>    // Before:<br>    fun authenticateLegacy(user: User, token: String): Boolean {<br>        val isValid = verifyToken(user.email, token)<br>        if (isValid) {<br>            &quot;Auth.success&quot;<br>        } else {<br>            &quot;Auth.failure&quot;<br>        }<br>        return isValid<br>    }<br><br>    // Now:<br>    @OptIn(ExperimentalStdlibApi::class)<br>    fun authenticate(user: User, token: String): Boolean =<br>        verifyToken(user.email, token)<br>            .onTrue { <br>                &quot;Auth.success&quot; <br>            }<br>            .onFalse { <br>                &quot;Auth.failure&quot;<br>            }<br><br>    <br>}</pre><h3>ifOrNull()</h3><ul><li>When shaping API responses or DTOs, we may return one computed value if a flag is true and another if false. Traditionally, this looks like 👇</li></ul><pre>val phoneNumber = if (user.hasContactInfo) user.phoneNumber else null</pre><h3>The New Way: ifOrNull()</h3><p>ifOrNull(condition) { //.. } lazily evaluates the lambda block <strong>only</strong> if the condition evaluates to true. If false, it returns null immediately.</p><pre>@OptIn(ExperimentalStdlibApi::class)<br>val phoneNumber = ifOrNull(user.hasContactInfo) { <br>    user.phoneNumber <br>}</pre><h3>References</h3><p><a href="https://kotlinlang.org/docs/whatsnew-eap.html?utm_campaign=kotlin-releases&amp;utm_content=2-5-0-beta2&amp;utm_medium=social&amp;utm_source=twitter#standard-library">What&#39;s new in Kotlin 2.5.0-Beta2 | Kotlin</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7854207156ea" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/clean-up-your-conditional-logic-exploring-kotlin-2-5-0-beta2s-new-boolean-nullable-utilities-7854207156ea">Clean Up Your Conditional Logic: Exploring Kotlin 2.5.0-Beta2’s New Boolean &amp; Nullable Utilities</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Connect ESP32 to Wi-Fi on Android (The Right Way) | EasyEsp]]></title>
            <link>https://proandroiddev.com/how-to-connect-esp32-to-wi-fi-on-android-the-right-way-easyesp-b110317a5a41?source=rss----c72404660798---4</link>
            <guid isPermaLink="false">https://medium.com/p/b110317a5a41</guid>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[iot]]></category>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Arul mani]]></dc:creator>
            <pubDate>Fri, 06 Nov 2026 15:14:25 GMT</pubDate>
            <atom:updated>2026-11-06T15:14:24.022Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_fS0Qmh6V8oG19Hu" /><figcaption>Photo by <a href="https://unsplash.com/@vishnumaiea?utm_source=medium&amp;utm_medium=referral">Vishnu Mohanan</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Stop frustrating your IoT users. Build a seamless ESP32 Wi-Fi provisioning flow using Jetpack Compose, Ktor, and MVI Architecture.</p><p>The Painless Way to Connect ESP32 Devices to Wi-Fi on Android 📱⚡</p><p><strong>How to build seamless IoT provisioning flows using Jetpack Compose, Ktor, and MVI Architecture.</strong></p><p>If you’ve ever built an IoT product, you know the biggest hurdle isn’t writing the firmware or building the cloud backend — it’s the <strong>onboarding process</strong>. Getting a screenless microcontroller (like an ESP32) connected to a user’s home Wi-Fi network is notoriously frustrating.</p><p>Users have to leave your app, go to their phone’s Wi-Fi settings, connect to a temporary hotspot, ignore the “Internet may not be available” warnings, go back to the app, enter their credentials, and pray it works.</p><p>As Android developers, we can do better.</p><p>Today, I am open-sourcing <a href="https://github.com/codingwitharul/esay-esp"><strong>EasyESP Connect</strong></a> — a battle-tested Kotlin Multiplatform (KMM) Library, Android App, and companion ESP32 firmware that makes provisioning ESP32 devices incredibly simple, resilient, and fully automated.</p><p>The Android IoT Routing Problem 🚧</p><p>Before diving into the solution, let’s talk about why Android makes IoT provisioning so hard.</p><p>When your phone connects to an ESP32’s temporary Wi-Fi hotspot, that hotspot doesn’t have internet access. Modern Android versions (Android 9+) are smart — if they detect a Wi-Fi network has no internet, they will silently route all HTTP requests through the user’s Cellular Data instead.</p><p>This means when your app tries to send the Wi-Fi password to <a href="http://192.168.4.1,">http://192.168.4.1,</a> the request goes to the cellular tower, times out, and the setup fails.</p><p>The Solution: EasyESP Connect ✨</p><p>EasyESP Connect solves this (and many other edge cases) by taking full control of the Android Networking stack and providing a beautiful, state-driven Jetpack Compose UI.</p><p><strong>Key Features:</strong> * <strong>Automated Device Discovery:</strong> Passively scans for local ESP32 devices broadcasting setup SSIDs using Android’s WifiManager. * <strong>Programmatic Hotspot Binding:</strong> Uses WifiNetworkSpecifier (Android 10+) to securely connect the phone to the ESP32 <em>without leaving the app</em>, and explicitly binds the app’s process to the Wi-Fi network so HTTP requests don’t leak to cellular data. * <strong>Smart Fallbacks:</strong> If the OS blocks the automatic connection, it gracefully degrades to a guided manual connection screen. * <strong>ESP32 Dual Mode:</strong> The firmware runs in WIFI_AP_STA mode, acting as a configuration portal while simultaneously scanning the environment for home routers.</p><p>How to use it in your app 🛠️</p><p>I built EasyESP Connect to be as flexible as possible. Whether you want a plug-and-play UI or you want to build your own custom screens, integration takes minutes.</p><p><em>Method 1: The Drop-In Compose UI</em></p><p>If you are building a modern Jetpack Compose app, you can drop the pre-built EasyEspSetupUI directly into your screen. It handles permissions, Wi-Fi scanning, Lottie animations, and error states automatically.</p><pre>import com.easyesp.shared.ui.EasyEspSetupUI<br> import com.easyesp.shared.mvi.EasyEspConfig<br> <br> @Composable<br> fun ProvisioningScreen() {<br>     EasyEspSetupUI(config = EasyEspConfig(scanTimeoutMillis = 60_000L),<br>                 onSuccess = { <br>                    println(&quot;ESP Device Provisioned Successfully! 🎉&quot;) <br>                 },<br>     onError = { error -&gt; println(&quot;Setup Failed: $error&quot;) })<br> }</pre><p><em>Method 2: The Activity Result API</em></p><p>If your app is built with XML Views or Fragments, you can launch the Compose flow using a standard ActivityResultContract and just wait for the callback:</p><pre>val launcher = registerForActivityResult(EasyEspSetupContract()) { result -&gt;<br>   result.onSuccess { <br>   // Device is connected!<br>   }.onFailure { error -&gt;<br>   // Handle failure<br>   }<br> }<br> <br> // Start Setup Flow<br> launcher.launch(EasyEspConfig())</pre><p><em>Method 3: Headless MVI (Build Your Own UI)</em></p><p>For developers who want complete control over the pixel-perfect design of their app, EasyESP provides a headless <strong>MVI (Model-View-Intent) Session</strong>.</p><p>You simply send EasyEspIntents to the session and observe the EasyEspState. The library handles all the ugly networking, timeouts, and state transitions in the background.</p><pre>val session = EasyEsp.createSession(context, coroutineScope, EasyEspConfig())<br> <br> // 1. Observe State<br> lifecycleScope.launch {<br>   session.state.collect { state -&gt; <br>       when(state) {<br>       is EasyEspState.Discovering -&gt; showCustomRadarAnimation()<br>       is EasyEspState.EspDevicesFound -&gt; displayDeviceList(state.devices)<br>       is EasyEspState.Provisioning -&gt; showLoadingSpinner()<br>       is EasyEspState.Success -&gt; navigateToDashboard()<br>       // …<br>       }<br>   }<br> }<br> <br> // 2. Send Intents to control the flow<br> session.sendIntent(EasyEspIntent.CheckPreconditionsAndStart)</pre><p>The Firmware Side 🔌</p><h3>3. Flash the Firmware (Arduino Library)</h3><p>To make your ESP32 compatible, you can use the official EasyEspAndroid Arduino Library.</p><h4>Search LibraryManager “EasyEspAndroid” and install Simply</h4><h4>or</h4><ol><li>Download this repository as a ZIP file.</li><li>In Arduino IDE, go to Sketch -&gt; Include Library -&gt; Add .ZIP Library… and select the ZIP.</li><li>Open Arduino IDE and go to File -&gt; Examples -&gt; EasyEspAndroid -&gt; BasicSetup.</li><li>Select your ESP32 board and upload at 115200 baud.</li></ol><p>Here is how simple your ESP32 code becomes:</p><pre>#include &lt;EasyEspAndroid.h&gt;<br><br>EasyEspAndroid easyEsp;<br><br>void setup() {<br>  Serial.begin(115200);<br>  easyEsp.onConnected([](String ip) {<br>    Serial.println(&quot;Wi-Fi connected! IP: &quot; + ip);<br>  });<br>  easyEsp.begin(&quot;EasyEsp-Setup&quot;); // Start the hotspot<br>}<br>void loop() {<br>  easyEsp.loop(); // Handle incoming setup requests from Android<br>}</pre><p>Flash this to your ESP32 via the Arduino IDE. It uses a lightweight asynchronous WebServer to expose two endpoints: 1. GET /networks — Returns a JSON array of surrounding Wi-Fi networks and signal strengths (RSSI). 2. POST /credentials — Accepts the target ssid and password from the Android app, drops the temporary hotspot, and connects to the home router.</p><p>Because it uses standard HTTP (via Ktor on Android), you can easily extend this to pass user tokens, MQTT broker URLs, or custom sensor data during the setup phase!</p><p>Get Started Today 🚀</p><p>Building IoT products is hard enough; onboarding shouldn’t be.</p><p>You can find the complete source code, the KMM shared library, the Jetpack Compose app, and the ESP32 firmware on GitHub.</p><p>🔗 <strong>GitHub Repository:</strong> <a href="https://github.com/codingwitharul/esay-esp">codingwitharul/esay-esp</a> 📚 <strong>Full Documentation:</strong> <a href="https://codingwitharul.github.io/esay-esp/">EasyESP Documentation</a></p><p>If this helps you build your next hardware project, drop a star on the repo! Happy coding! 👨‍💻💡</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b110317a5a41" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/how-to-connect-esp32-to-wi-fi-on-android-the-right-way-easyesp-b110317a5a41">How to Connect ESP32 to Wi-Fi on Android (The Right Way) | EasyEsp</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>