<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged reactnative</title>
<link>https://coderlegion.com/tag/reactnative</link>
<description>Powered by Question2Answer</description>
<item>
<title>Two native apps into one React Native codebase in six months</title>
<link>https://coderlegion.com/30734/two-native-apps-into-one-react-native-codebase-in-six-months</link>
<description>&lt;p&gt;I owned mobile for Pulse, an offline-first inspection app for auditors, on Android and iOS, from an early prototype to 100+ B2B enterprise clients. For most of that time it was two native apps: Kotlin on Android, Swift on iOS, kept in feature parity. In 2025 I led a full rewrite of both into a single React Native codebase. I architected it and built it alongside the team, and we completed it in six months, before I left in May 2025.&lt;/p&gt;
&lt;p&gt;This isn’t a “native versus React Native” post. The native apps worked. The earlier posts onPulse’s sync engine and ona local-only app cover why they were built the way they were. This one is about the rewrite: why we did it, what we kept, and how it reached users.&lt;/p&gt;
&lt;p&gt;Why rewrite two apps that worked&lt;br&gt;
Keeping two native apps in feature parity has a simple cost: every feature is built twice, once in Kotlin and once in Swift, and every fix has to land in both. That was the reason for the rewrite, in two parts:&lt;/p&gt;
&lt;p&gt;One codebase to maintain instead of two.&lt;br&gt;
Shipping faster , because a feature or a fix reaches Android and iOS at the same time.&lt;br&gt;
Neither reason says the native apps were bad. They say that keeping two of them had a cost, and one codebase removes it.&lt;/p&gt;
&lt;p&gt;Replicate the behaviour, don’t redesign it&lt;br&gt;
The native apps’ behaviour had years of field use behind it: records that get a temporary local ID and are remapped to a server ID when they sync, photos uploaded before the form that references them, upload progress the auditor can see, and inspection forms built dynamically. So the React Native app replicated that behaviour rather than redesigning it.&lt;/p&gt;
&lt;p&gt;The alternative is harder than it looks. A rewrite that also changes how sync works is two projects at once, and every difference between the old app and the new one is something to explain to an auditor in the field. Replicating proven behaviour keeps the rewrite to one question: does the new app do what the old one did?&lt;/p&gt;
&lt;p&gt;I built the core, the team built on top&lt;br&gt;
I set the architecture and built the core myself: sync, offline storage and uploads. The team built the screens and features on top of it.&lt;/p&gt;
&lt;p&gt;In an offline-first app, that core is where the risk sits. Everything on screen depends on the core behaving the way the native apps did, and in an offline-first app the core is where data gets lost if something is wrong. Screens are easier to see, test and fix; a sync bug can hide until an auditor’s work goes missing.&lt;/p&gt;
&lt;p&gt;Rolling out gradually&lt;br&gt;
The new app didn’t replace the native ones for everyone at once. It rolled out gradually, with the native app still available while users moved over.&lt;/p&gt;
&lt;p&gt;For an offline-first app that isn’t caution for its own sake. Auditors work without signal, so at any moment someone may have work on the phone that hasn’t reached the server yet. A gradual rollout means a problem in the new app reaches a few users rather than all of them, and there is still a working app to fall back on while it’s fixed.&lt;/p&gt;
&lt;p&gt;What I’d tell another team&lt;br&gt;
If you’re rewriting an offline-first app, roll it out gradually. Keep the old app available and move users over in stages. A big-bang switch bets every auditor’s unsynced work on the new app being right on day one.&lt;/p&gt;
&lt;p&gt;More on Pulse is in the case study.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30734/two-native-apps-into-one-react-native-codebase-in-six-months</guid>
<pubDate>Sun, 11 Oct 2026 18:35:56 +0000</pubDate>
</item>
<item>
<title>The bug that survived being recycled: automaticallyAdjustKeyboardInsets and Fabric view recycling on iOS</title>
<link>https://coderlegion.com/30246/that-survived-being-recycled-automaticallyadjustkeyboardinsets-fabric-view-recycling</link>
<description>&lt;p&gt;After logging a meal in &lt;a rel=&quot;nofollow&quot; href=&quot;https://pragathijayaram.com/portfolio/&quot;&gt;Anovi&lt;/a&gt;, the nutrition app I build, the next screen opened normally. The content was there. Then the first scroll ran past the end of it, into empty space that should not have existed, and stopped.&lt;/p&gt;
&lt;p&gt;Dragging in that space did nothing. Not a slow scroll, not a bounce. Nothing moved, because there was nothing under my finger. The content was somewhere above me and there was no way to reach it. The only way out was to kill the app.&lt;/p&gt;
&lt;p&gt;It was not every screen and not every time, which is what kept it alive through earlier rounds of testing. It produced no crash, no error, and no failed request.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;In short.&lt;/strong&gt; On iOS with the New Architecture, a scroll view that opted into keyboard inset handling can leave that behaviour switched on when React Native recycles it. The next screen to inherit that view gets a phantom inset it never asked for. Filed as &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/facebook/react-native/issues/57755&quot;&gt;react-native#57755&lt;/a&gt;. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;If you just want to check and fix your own app, that is &lt;a rel=&quot;nofollow&quot; href=&quot;https://pragathijayaram.com/articles/react-native-keyboard-insets-leaking/&quot;&gt;a separate, shorter piece&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-evidence-that-was-missing&quot;&gt;The evidence that was missing&lt;/h3&gt;
&lt;p&gt;Every request in the sequence returned what it should, in the time it should. Nothing failed, nothing timed out, nothing appeared in the logs at the moment things went wrong. The app had not crashed and was not in an error state.&lt;/p&gt;
&lt;p&gt;Healthy logs are usually read as the absence of a finding. Here they were the finding, because they eliminated the entire category I had been searching in. That is what promoted the next detail from background noise to the only lead.&lt;/p&gt;
&lt;p&gt;The next detail was the keyboard. On the broken screens it appeared, briefly, and went away again. On a screen with no text input on it and nothing focused.&lt;/p&gt;
&lt;p&gt;A keyboard reacting where there is nothing to type into is not a rendering bug. Something was still listening.&lt;/p&gt;
&lt;h3 id=&quot;the-screen-that-broke-was-not-the-screen-at-fault&quot;&gt;The screen that broke was not the screen at fault&lt;/h3&gt;
&lt;p&gt;React Native's new architecture recycles native views. Rather than destroying a component's native view and building a fresh one, it resets the existing view and hands it to the next component that needs the same kind. On iOS, that reset happens in a method called &lt;code&gt;prepareForRecycle&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Recycling is only safe if the reset is complete. Anything left behind belongs to whoever inherits the view next.&lt;/p&gt;
&lt;p&gt;A scroll view can opt into having iOS adjust its insets when the keyboard moves, with a prop called &lt;code&gt;automaticallyAdjustKeyboardInsets&lt;/code&gt;. Underneath, that sets a flag on the native view and gates a keyboard observer that lives for as long as the view does. &lt;code&gt;prepareForRecycle&lt;/code&gt; does not clear that flag.&lt;/p&gt;
&lt;p&gt;So the broken screen was innocent. It had no text input, no keyboard props, and nothing to do with any of this. It had simply been handed a view that came out of the pool still holding someone else's settings.&lt;/p&gt;
&lt;h4 id=&quot;where-the-keyboard-came-from&quot;&gt;Where the keyboard came from&lt;/h4&gt;
&lt;p&gt;The view came from the meal logging modal, which does have a scroll view with the prop set and a search field that focuses itself.&lt;/p&gt;
&lt;p&gt;The interesting part is what happened when I closed it. Tapping Done dismissed the keyboard, reset the modal's internal flow back to its search step, and hid the modal, all in the same render batch. Resetting the flow remounted the search field, its &lt;code&gt;autoFocus&lt;/code&gt; fired, and &lt;strong&gt;the app opened a keyboard inside a modal that was already closing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That is the flicker. Not a leftover from typing, but a keyboard my own code summoned into a dying screen. Its scroll view then went into the recycle pool while that keyboard was still up.&lt;/p&gt;
&lt;p&gt;None of that is a bug. A keyboard being up while a screen goes away is ordinary, and the framework is meant to survive it. What it did instead was hand the next screen a view that was still holding the last one's settings.&lt;/p&gt;
&lt;h3 id=&quot;building-something-that-fails-on-demand&quot;&gt;Building something that fails on demand&lt;/h3&gt;
&lt;p&gt;An explanation that only reproduces inside a nutrition app is not much use to anyone, and it is not something a framework maintainer can act on. So the next step was the smallest thing that still fails: a modal with the prop and a focused input that kills itself mid-animation, then a plain scroll view that mounts straight afterwards and reports its own geometry.&lt;/p&gt;
&lt;h4 id=&quot;the-detector-that-cried-wolf&quot;&gt;The detector that cried wolf&lt;/h4&gt;
&lt;p&gt;Getting the measurement right took longer than finding the bug.&lt;/p&gt;
&lt;p&gt;The first version reported failures that were not failures. A scroll view settling after a bounce moves on its own, and sampling at the wrong moment records that jitter as displacement. Several iterations went into separating &quot;this view is still settling&quot; from &quot;this view has been moved by something else&quot;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A detector that reports a bug which is not there is worse than no detector, because it makes the real signal unfalsifiable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everything downstream of it, including the controls, would have been noise interpreted as evidence.&lt;/p&gt;
&lt;p&gt;Once it was honest, the numbers were unambiguous. A scroll view that never asked for keyboard insets, with the keyboard hidden, reporting a bottom inset of 217.3 points. Its content was 840 points tall in a 234 point window, so the furthest it should ever scroll is 606. It went to 823.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pragathijayaram.com/media/react-native-scrollview-recycling-bug/harness-pool-dirty.png&quot; alt=&quot;The reproducer on React Native 0.86.2 showing a victim scroll view reporting a bottom inset of 217.3 with the keyboard hidden, content 840 and viewport 234, alongside counters for cycles, rogue keyboard events and corrupt readings.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Physical iPhone, iOS 26.5.2, Release build, React Native 0.86.2, New Architecture. The victim sets no keyboard props of any kind.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-controls-are-the-argument&quot;&gt;The controls are the argument&lt;/h3&gt;
&lt;p&gt;A reproducer that fails is a claim. A reproducer that fails, and stops failing when you remove exactly one ingredient, is evidence.&lt;/p&gt;
&lt;p&gt;Two controls, each from a freshly killed process. Turn off the inset prop: nothing. Dismiss the keyboard and let it settle before the modal closes: nothing. Each ingredient is necessary and neither is sufficient. The failure needs a view configured for keyboard insets to be recycled while its observer is still live.&lt;/p&gt;
&lt;h3 id=&quot;measuring-it-instead-of-reasoning-about-it&quot;&gt;Measuring it instead of reasoning about it&lt;/h3&gt;
&lt;p&gt;Everything so far was inference, from reading source and watching behaviour. The mechanism deserved a measurement, so I instrumented the framework itself: every keyboard delivery logged with the view's address and whether it was on screen or in the recycle pool.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;rec-in  view=0x108118c00 ivar=1          entering prepareForRecycle
rec-out view=0x108118c00 ivar=1          leaving it, flag still set
kbd     view=0x108118c00 ivar=1 POOLED
write   view=0x108118c00 bottom=217.3 POOLED    ‚Üê inset written while pooled
props   view=0x108118c00 old=1 new=0 ivar=0     ‚Üê next screen mounts, flag cleared
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The write lands &lt;strong&gt;while the view is sitting in the pool&lt;/strong&gt;, 105ms before anything mounts on it. When a screen does mount, the flag is cleared immediately, so the screen you can see is never written to at all.&lt;/p&gt;
&lt;p&gt;That ruled out the explanation I had been carrying. An earlier test, where a poisoned view's scroll position moved after I focused an unrelated text field, had looked like proof that the recycled view was still listening while mounted. It was not. The keyboard changed the layout, the window grew from 234 points to 299, the furthest legal scroll position dropped by 65, and iOS pulled the offset back to the new limit. Arithmetic, not a live listener.&lt;/p&gt;
&lt;h3 id=&quot;what-is-actually-happening&quot;&gt;What is actually happening&lt;/h3&gt;
&lt;p&gt;The pooled view keeps receiving keyboard notifications and keeps writing insets onto itself. It is not stale; it is current. It tracks the keyboard continuously, taking a real value when a keyboard rises and zero when it falls.&lt;/p&gt;
&lt;p&gt;Whatever value it happens to be holding at the moment someone inherits it is what that screen is stuck with, because the inset is only reassigned when a component's props change it, and a component that never set the prop has nothing to reassign.&lt;/p&gt;
&lt;p&gt;Which means the whole thing turns on timing. Sweeping the delay before the modal tears itself down, ten cycles each from a fresh process:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;modal torn down after &lt;/th&gt;&lt;th&gt;corrupt readings &lt;/th&gt;&lt;th&gt;phantom inset&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;~120ms &lt;/td&gt;&lt;td&gt;36 &lt;/td&gt;&lt;td&gt;217.3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;~220ms &lt;/td&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;0.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;~320ms &lt;/td&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;0.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;~520ms &lt;/td&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;0.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;keyboard dismissed first &lt;/td&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;0.0&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Only the earliest teardown reproduces. Wait longer and the keyboard's own disappearance reaches the pooled view first, zeroes the inset, and the pool quietly cleans itself. The bug needs a screen to mount during the narrow window while a keyboard is up.&lt;/p&gt;
&lt;p&gt;That is also why it was intermittent in production, and why it survived earlier testing rounds. Most of the time nothing mounts inside that window.&lt;/p&gt;
&lt;h4 id=&quot;why-it-could-not-be-scrolled-back&quot;&gt;Why it could not be scrolled back&lt;/h4&gt;
&lt;p&gt;The phantom inset extends the scrollable range past the end of the content, and that extra region has no view underneath it. So it takes no touches. A drag there produces no scroll events at all.&lt;/p&gt;
&lt;p&gt;In the reproducer a sliver of the last row stays visible, so you can grab that and pull yourself back. In the app the scroll views were full screen. Once the content had left the viewport there was nothing to grab, and no gesture in the app could recover it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A screen recording of the reproducer on a physical device is on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://pragathijayaram.com/articles/react-native-scrollview-recycling-bug/&quot;&gt;original article&lt;/a&gt;: several poison cycles, a scroll up into the space that should not exist, then taps on an unrelated input.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the difference between a layout glitch and a screen that is finished until you kill the process.&lt;/p&gt;
&lt;h3 id=&quot;what-happened-after-filing&quot;&gt;What happened after filing&lt;/h3&gt;
&lt;p&gt;I reproduced it unchanged on React Native 0.86.2, the latest release at the time, and filed it as &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/facebook/react-native/issues/57755&quot;&gt;react-native#57755&lt;/a&gt; with a &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/PragathiJ/rn-aaki-recycle-repro&quot;&gt;public reproducer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Two pull requests followed, from people I have never met. The second carries a regression test covering the whole lifecycle, and a Meta engineer imported it for internal review the same morning it appeared.&lt;/p&gt;
&lt;p&gt;Its two lines are small: clear the flag when the view is recycled, and stop guarding the assignment behind a props comparison so a recycled view can be re-armed properly. I applied them to the reproducer and ran it on the phone that found the bug in the first place. Baseline immediately before: 36 corrupt readings in 10 cycles. With the patch: 49 cycles, zero.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-take-from-this&quot;&gt;What I would take from this&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The healthy logs were the finding.&lt;/strong&gt; They eliminated a whole category, which is what turned a flicker into the only lead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Controls before claims.&lt;/strong&gt; Any bug can be explained by a plausible story. The question is which single ingredient you can remove to make it stop, and whether you have actually tried removing it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A detector that lies is worse than no detector.&lt;/strong&gt; Most of the reproducer work went into eliminating false positives, not into finding the failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Instrument the explanation, not just the bug.&lt;/strong&gt; A story that fits every observation is not the same as a correct one. Mine was in the right place and the wrong moment, and only logging the framework showed the difference.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you think your own app has this, the checks and the three ways to fix it are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://pragathijayaram.com/articles/react-native-keyboard-insets-leaking/&quot;&gt;a shorter companion piece&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The reproducer is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/PragathiJ/rn-aaki-recycle-repro&quot;&gt;github.com/PragathiJ/rn-aaki-recycle-repro&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;I write about production engineering and applied AI at &lt;a rel=&quot;nofollow&quot; href=&quot;https://pragathijayaram.com/articles/&quot;&gt;pragathijayaram.com&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30246/that-survived-being-recycled-automaticallyadjustkeyboardinsets-fabric-view-recycling</guid>
<pubDate>Thu, 08 Oct 2026 09:12:17 +0000</pubDate>
</item>
<item>
<title>automaticallyAdjustKeyboardInsets is leaking into your other screens: how to check and fix it</title>
<link>https://coderlegion.com/30241/automaticallyadjustkeyboardinsets-is-leaking-into-your-other-screens-how-check-and-fix</link>
<description>&lt;p&gt;A screen opens normally, then scrolls past the end of its content into empty&lt;br&gt;
space, and dragging in that space does nothing at all. Restarting the app is the&lt;br&gt;
only way out.&lt;/p&gt;
&lt;p&gt;If that sounds familiar, this is a React Native framework defect, not your code.&lt;br&gt;
This page is the operational version: how to confirm it, how to find every source&lt;br&gt;
in your project, and what to do. The investigation behind it, including why it&lt;br&gt;
took a while to understand, &lt;a rel=&quot;nofollow&quot; href=&quot;https://pragathijayaram.com/articles/react-native-scrollview-recycling-bug/&quot;&gt;is here&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Scope.&lt;/strong&gt; iOS only, New Architecture only. The code involved is the Fabric iOS&lt;br&gt;
scroll view, so the old architecture and Android take different paths.&lt;br&gt;
Reproduced on React Native 0.81.4 and 0.86.2. Filed as&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/facebook/react-native/issues/57755&quot;&gt;react-native#57755&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-is-going-wrong&quot;&gt;What is going wrong&lt;/h3&gt;
&lt;p&gt;A scroll view that sets &lt;code&gt;automaticallyAdjustKeyboardInsets&lt;/code&gt; keeps that behaviour&lt;br&gt;
switched on when React Native recycles its native view. While the view sits in the&lt;br&gt;
recycle pool it carries on receiving keyboard notifications and writing insets&lt;br&gt;
onto itself. The next component to be handed that view inherits whatever value was&lt;br&gt;
there, and nothing puts it back, because a component that never set the prop has&lt;br&gt;
no inset of its own to assign.&lt;/p&gt;
&lt;p&gt;The result is a scroll view with extra scrollable space below its content. That&lt;br&gt;
space has no view underneath it, so it takes no touches, which is why you cannot&lt;br&gt;
drag your way back out of it.&lt;/p&gt;
&lt;h3 id=&quot;confirm-it&quot;&gt;Confirm it&lt;/h3&gt;
&lt;p&gt;The first sign is a keyboard appearing on a screen with nothing focused. That is&lt;br&gt;
the tell, not the bug.&lt;/p&gt;
&lt;p&gt;The test is to read the inset on a scroll view that should not have one. Put this&lt;br&gt;
on a screen that sets no insets of its own, scroll it with the keyboard hidden,&lt;br&gt;
and watch the console:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const reported = React.useRef(false);

&amp;lt;ScrollView
  scrollEventThrottle={16}
  onScroll={(e) =&amp;gt; {
    // contentInset is iOS-only; on Android nativeEvent has no such field.
    const bottom = e.nativeEvent.contentInset?.bottom ?? 0;
    if (bottom &amp;gt; 0 &amp;amp;&amp;amp; !reported.current) {
      reported.current = true;
      console.warn('phantom bottom inset', bottom);
    }
  }}
&amp;gt;
  &amp;lt;View style={{ height: 2000 }} /&amp;gt;
&amp;lt;/ScrollView&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details decide whether this test is worth anything. The scroll view needs&lt;br&gt;
content taller than itself, or it never scrolls, &lt;code&gt;onScroll&lt;/code&gt; never fires, and you&lt;br&gt;
will conclude you are clean without having tested anything. And the reading has to&lt;br&gt;
be gated, because at a 16ms throttle an ungated &lt;code&gt;console.warn&lt;/code&gt; buries your log.&lt;/p&gt;
&lt;p&gt;A scroll view that never opted in should report zero. Anything else means it is&lt;br&gt;
sitting on a view that came out of the pool still armed.&lt;/p&gt;
&lt;h3 id=&quot;find-every-source&quot;&gt;Find every source&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -rn &quot;automaticallyAdjustKeyboardInsets&quot; src/
grep -rn &quot;contentInset&quot; src/   # also catches contentInsetAdjustmentBehavior
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second search matters more than it looks. Any prop that writes native insets&lt;br&gt;
belongs to the native view, and therefore to the recycling pool, so a static&lt;br&gt;
&lt;code&gt;contentInset&lt;/code&gt; combined with an automatic &lt;code&gt;contentInsetAdjustmentBehavior&lt;/code&gt; is the&lt;br&gt;
same class of risk even though it has nothing to do with keyboards. In my own&lt;br&gt;
sweep that turned up a grid component the first search had missed.&lt;/p&gt;
&lt;p&gt;Then run both again against &lt;code&gt;node_modules&lt;/code&gt;. The pool is per process, not per&lt;br&gt;
module, so a scroll view inside a dependency arms it exactly as well as one of&lt;br&gt;
yours. You can clean your own code completely and still be handed a poisoned view by a library you did not write.&lt;/p&gt;
&lt;h3 id=&quot;fix-it&quot;&gt;Fix it&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Option 1, remove the prop.&lt;/strong&gt; Handle keyboard layout explicitly instead. This is&lt;br&gt;
what I shipped.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before: the flag does the layout, and the native view keeps listening
&amp;lt;Modal visible={visible}&amp;gt;
  &amp;lt;ScrollView automaticallyAdjustKeyboardInsets&amp;gt;
    &amp;lt;TextInput autoFocus /&amp;gt;
  &amp;lt;/ScrollView&amp;gt;
&amp;lt;/Modal&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After: explicit layout, nothing left armed on the native view
&amp;lt;Modal visible={visible}&amp;gt;
  &amp;lt;KeyboardAvoidingView behavior=&quot;padding&quot; style={{ flex: 1 }}&amp;gt;
    &amp;lt;ScrollView contentContainerStyle={{ paddingBottom: 24 }}&amp;gt;
      &amp;lt;TextInput autoFocus={visible} /&amp;gt;
    &amp;lt;/ScrollView&amp;gt;
  &amp;lt;/KeyboardAvoidingView&amp;gt;
&amp;lt;/Modal&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;autoFocus={visible}&lt;/code&gt; change matters more than it looks. Focusing on mount is&lt;br&gt;
what starts a keyboard animation during modal presentation, which is exactly the&lt;br&gt;
window the defect needs. Watch for the same shape anywhere a modal resets its&lt;br&gt;
internal state as it closes: that can remount a focused field and open a keyboard&lt;br&gt;
inside a screen that is already going away.&lt;/p&gt;
&lt;p&gt;One caveat: &lt;code&gt;KeyboardAvoidingView&lt;/code&gt; with &lt;code&gt;behavior=&quot;padding&quot;&lt;/code&gt; inside a &lt;code&gt;Modal&lt;/code&gt;&lt;br&gt;
behaves differently depending on presentation style and often wants a&lt;br&gt;
&lt;code&gt;keyboardVerticalOffset&lt;/code&gt;. Treat it as a shape to adapt, not a drop-in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 2, keep the prop but dismiss the keyboard first.&lt;/strong&gt; Dismiss it and let it&lt;br&gt;
settle before the screen unmounts. This came back at zero in the reproducer. It is&lt;br&gt;
narrower than option 1, because it depends on every exit path from that screen&lt;br&gt;
doing the same thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 3, patch the framework locally.&lt;/strong&gt; Until the upstream fix ships you can&lt;br&gt;
carry it as a &lt;code&gt;patch-package&lt;/code&gt; patch. In &lt;code&gt;RCTScrollViewComponentView.mm&lt;/code&gt;, clear the&lt;br&gt;
flag on recycle and assign it on every update instead of only when it changes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-objc&quot;&gt;// prepareForRecycle: disarm before the view goes back in the pool
_automaticallyAdjustKeyboardInsets = NO;

// updateProps: drop the old-vs-new condition around the assignment
_automaticallyAdjustKeyboardInsets = newScrollViewProps.automaticallyAdjustKeyboardInsets;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both lines are needed. The first stops the leak. The second matters because&lt;br&gt;
&lt;code&gt;prepareForRecycle&lt;/code&gt; does not reset the stored props, so once the flag is cleared a&lt;br&gt;
view being reused by another component that &lt;em&gt;also&lt;/em&gt; wants keyboard insets would&lt;br&gt;
skip the assignment and silently lose the behaviour.&lt;/p&gt;
&lt;p&gt;This is the substance of &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/facebook/react-native/pull/57811&quot;&gt;react-native#57811&lt;/a&gt;, which a Meta engineer has imported for internal review. I verified those changes against the reproducer on a physical device: 49 cycles, zero failures, where the unpatched build produced 36 failures in 10 cycles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you are on 0.86.2 and patching the framework&lt;/strong&gt;, note that React core ships&lt;br&gt;
prebuilt, so edits to &lt;code&gt;RCTScrollViewComponentView.mm&lt;/code&gt; in &lt;code&gt;node_modules&lt;/code&gt; never&lt;br&gt;
reach the binary. You need &lt;code&gt;RCT_USE_PREBUILT_RNCORE=0&lt;/code&gt; and a fresh &lt;code&gt;pod install&lt;/code&gt;,&lt;br&gt;
or the patch silently does nothing.&lt;/p&gt;
&lt;h3 id=&quot;what-does-not-work&quot;&gt;What does not work&lt;/h3&gt;
&lt;p&gt;Scrolling the screen back to the top when it opens. I shipped that early and it&lt;br&gt;
does clear the wrong opening position, which is enough to make it look like a&lt;br&gt;
cure. It does not remove the phantom inset, so the dead space is still waiting at&lt;br&gt;
the bottom.&lt;/p&gt;
&lt;h3 id=&quot;how-to-tell-you-have-fixed-it&quot;&gt;How to tell you have fixed it&lt;/h3&gt;
&lt;p&gt;Run the confirmation check again after the flow that used to break things. Zero&lt;br&gt;
inset, and a scroll to the bottom that stops at the end of your content with&lt;br&gt;
nothing beneath it.&lt;/p&gt;
&lt;p&gt;The reproducer is at&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/PragathiJ/rn-aaki-recycle-repro&quot;&gt;github.com/PragathiJ/rn-aaki-recycle-repro&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I write about production engineering and applied AI at&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://pragathijayaram.com/articles/&quot;&gt;pragathijayaram.com&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30241/automaticallyadjustkeyboardinsets-is-leaking-into-your-other-screens-how-check-and-fix</guid>
<pubDate>Thu, 08 Oct 2026 08:56:07 +0000</pubDate>
</item>
<item>
<title>Native, Kotlin Multiplatform or React Native: how I'd choose</title>
<link>https://coderlegion.com/29953/native-kotlin-multiplatform-or-react-native-how-id-choose</link>
<description>&lt;hr&gt;
&lt;p&gt;title: Native, Kotlin Multiplatform or React Native: how I'd choose&lt;br&gt;
published: true&lt;br&gt;
date: 2026-09-25 00:00:00 UTC&lt;br&gt;
tags: kotlinmultiplatform,reactnative,android,ios&lt;/p&gt;
&lt;h3 id=&quot;canonical-url-https-abhishekgoyal-me-writing-native-kmp-or-react-native&quot;&gt;canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://abhishekgoyal.me/writing/native-kmp-or-react-native&quot;&gt;https://abhishekgoyal.me/writing/native-kmp-or-react-native&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;“Should we build this in Kotlin Multiplatform or React Native?” is a question teams often ask before anyone has written down what the app has to do. That’s backwards. I’ve shipped all three approaches to production: native Android and iOS apps, a shared Kotlin Multiplatform data layer in HelperBook, and a React Native rewrite of two native apps at Pulse. The framework has never been the first decision. The features are.&lt;/p&gt;
&lt;h3 id=&quot;start-with-the-features-not-the-framework&quot;&gt;Start with the features, not the framework&lt;/h3&gt;
&lt;p&gt;Before choosing anything, I list what the app actually has to do, and which of those things lean on the platform. Camera capture, video playback, background work, offline data, sensors, OS integrations. Then I weigh each option against that list, along with the team that will maintain it and the time and money available.&lt;/p&gt;
&lt;p&gt;Most of the heat in “Kotlin Multiplatform vs React Native” debates comes from people comparing the tools in the abstract. For a specific app, the answer is usually much clearer than the debate suggests.&lt;/p&gt;
&lt;h3 id=&quot;when-i-d-stay-fully-native&quot;&gt;When I’d stay fully native&lt;/h3&gt;
&lt;p&gt;Sharing code is not free, so sometimes the right amount to share is none.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The market is on one platform.&lt;/strong&gt; HelperBook is for Indian households, where Android is the phone most people carry, so it shipped on Android first. Its records still live in a shared Kotlin Multiplatform module with SQLDelight, so an iOS app could reuse that data layer rather than rewrite it, but the app itself is native Android.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The app leans hard on platform APIs.&lt;/strong&gt; On a video-first hiring platform I built 100% of the Android client, and the core of it was CameraX capture, Media3 playback and on-device speech transcription. That is exactly the kind of work where you want the platform’s own APIs directly, not through a layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The two apps look and behave differently.&lt;/strong&gt; If the Android and iOS versions don’t share much UI or flow, a shared UI codebase saves less than it costs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-kotlin-multiplatform-fits&quot;&gt;When Kotlin Multiplatform fits&lt;/h3&gt;
&lt;p&gt;Kotlin Multiplatform shares business logic and data while each platform keeps a fully native UI. I reach for it when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Native apps already exist.&lt;/strong&gt; You can add a shared module to the apps you have and move code into it gradually, instead of rewriting both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The team writes Kotlin.&lt;/strong&gt; Android developers can own the shared code without learning a new stack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The business logic is complex and must match on both platforms.&lt;/strong&gt; Rules, calculations and offline data that behave slightly differently on Android and iOS are a bug factory. Writing them once removes a whole class of problems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native feel matters.&lt;/strong&gt; The UI stays Jetpack Compose on Android and SwiftUI or UIKit on iOS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-react-native-fits&quot;&gt;When React Native fits&lt;/h3&gt;
&lt;p&gt;React Native gives you one codebase for the UI as well as the logic. I recommend it when the timeline is short and the budget is small, and one team has to ship both platforms quickly. It also makes more sense when the screens are the same on both platforms, and when there’s already a React web team whose skills carry over.&lt;/p&gt;
&lt;h3 id=&quot;performance-honestly&quot;&gt;Performance, honestly&lt;/h3&gt;
&lt;p&gt;People search for “Kotlin Multiplatform vs React Native performance” a lot, so here’s my view. Kotlin Multiplatform keeps the UI fully native, and native UI is smoother. For most business apps (forms, lists, dashboards) React Native is fine, and the difference users feel comes far more from the architecture than from the framework.&lt;/p&gt;
&lt;p&gt;I haven’t shipped a Flutter app, so I won’t argue for or against it from experience. It’s a real option; it’s just not one I can speak to first-hand.&lt;/p&gt;
&lt;h3 id=&quot;what-pulse-taught-me&quot;&gt;What Pulse taught me&lt;/h3&gt;
&lt;p&gt;At Pulse I owned mobile, Android and iOS, from an early prototype to 100+ B2B enterprise clients. For years it ran as two native apps, Kotlin on Android and Swift on iOS, kept at feature parity. Keeping two native apps at parity meant building every feature twice.&lt;/p&gt;
&lt;p&gt;In 2025 I led the rewrite of both apps into one React Native codebase, architected and co-built with the team in six months, and it replaced both native apps in production. React Native fitted the situation: one codebase suited the timeline and budget, the web app was already built in React, and the screens were the same on both platforms.&lt;/p&gt;
&lt;p&gt;The hardest part of the port was reliable background uploads, which the offline engine depended on. The inspections could run to 500+ questions and 1,000+ photos, captured offline and uploaded in the background once a connection came back, so the upload behaviour had to be as dependable after the rewrite as before it.&lt;/p&gt;
&lt;p&gt;Looking back, React Native was a reasonable call for that timeline, budget and team. Knowing what I know now, I’d weigh Kotlin Multiplatform harder for an app with that much offline logic. The sync rules and the upload queue are exactly the kind of code you want written once and shared, while each platform keeps its own native background APIs.&lt;/p&gt;
&lt;h3 id=&quot;how-i-d-decide-for-your-app&quot;&gt;How I’d decide for your app&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;If your app… &lt;/th&gt;&lt;th&gt;I’d lean towards&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Targets one platform first, or leans hard on camera, video, sensors &lt;/td&gt;&lt;td&gt;Native&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Already has native apps, or complex logic that must match on both &lt;/td&gt;&lt;td&gt;Kotlin Multiplatform&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Needs both platforms fast, on a small budget, with the same screens &lt;/td&gt;&lt;td&gt;React Native&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;None of these is a rule. They’re where I start, and the feature list decides the rest. If you’re weighing this for a real app, &lt;a rel=&quot;nofollow&quot; href=&quot;https://abhishekgoyal.me/services/kmp&quot;&gt;here’s how I help teams adopt Kotlin Multiplatform&lt;/a&gt;, and every engagement starts with a paid discovery sprint that ends in a written recommendation you can use with anyone.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29953/native-kotlin-multiplatform-or-react-native-how-id-choose</guid>
<pubDate>Tue, 06 Oct 2026 08:23:00 +0000</pubDate>
</item>
<item>
<title>Why I built Animal Sudoku without a single ad</title>
<link>https://coderlegion.com/29236/why-i-built-animal-sudoku-without-a-single-ad</link>
<description>&lt;p&gt;Screen time for children is broken. Most free games trade a childs focus for ad revenue. Flashing banners and loud interruptions shatter concentration during critical learning moments. I built Animal Sudoku Kids Puzzle to be a safe haven for logic.&lt;/p&gt;
&lt;h4 id=&quot;the-problem-that-bothered-me&quot;&gt;The problem that bothered me&lt;/h4&gt;
&lt;p&gt;If you've ever handed your phone or tablet to a child, you know the script:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &quot;free&quot; puzzle game opens with a 30-second video ad.&lt;/li&gt;
&lt;li&gt;A flashing &quot;UPGRADE NOW&quot; button takes up a third of the screen.&lt;/li&gt;
&lt;li&gt;A pop-up appears mid-game asking the child to rate the app.&lt;/li&gt;
&lt;li&gt;And somewhere, quietly, an SDK is collecting behavioral data on a 5-year-old.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm a parent. I'm also an engineer. The combination made me increasingly uncomfortable with what I was seeing — apps designed to capture and monetize children's attention rather than support their development.&lt;/p&gt;
&lt;p&gt;I kept thinking: &lt;em&gt;the technology to do this better has existed for a decade. Why isn't anyone just… doing it?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So I built it.&lt;/p&gt;
&lt;h4 id=&quot;what-animal-sudoku-is-and-isn-t-&quot;&gt;What Animal Sudoku is (and isn't)&lt;/h4&gt;
&lt;p&gt;It's a kid-first introduction to Sudoku, with three difficulty levels — a gentle 4×4 grid for beginners, a harder 4×4 for the in-between phase, and a proper 6×6 for puzzle-loving 8+ year olds. Six animals, color-coded zones, smart hints when stuck, auto-save so progress is never lost, gentle background music, and a confetti celebration when you finish a puzzle.&lt;/p&gt;
&lt;p&gt;There is no &quot;Game Over.&quot; There's no time pressure unless the child wants to chase a score. There's no failure state — just exploration.&lt;/p&gt;
&lt;h4 id=&quot;what-it-isn-t-&quot;&gt;What it &lt;strong&gt;isn't&lt;/strong&gt;:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;❌ No advertising. None. Anywhere.&lt;/li&gt;
&lt;li&gt;❌ No third-party analytics or tracking SDKs.&lt;/li&gt;
&lt;li&gt;❌ No account creation or sign-up.&lt;/li&gt;
&lt;li&gt;❌ No internet connection required to play.&lt;/li&gt;
&lt;li&gt;❌ No &quot;watch a video to unlock&quot; mechanics.&lt;/li&gt;
&lt;li&gt;❌ No data collected from your child. None. The app is fully COPPA-compliant by virtue of collecting nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The full game is free. There are no required purchases. Future optional theme packs may exist, but the core game stays complete and free forever.&lt;/p&gt;
&lt;h4 id=&quot;engineering-for-every-child-accessibility-first&quot;&gt;Engineering for Every Child: Accessibility first&lt;/h4&gt;
&lt;p&gt;Inclusion was not a nice-to-have feature. It was a core requirement from day one. I spent a significant portion of development time ensuring that children with different physical or visual needs could play independently.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VoiceOver and TalkBack Support: Every animal icon and grid cell contains a semantic accessibility label.&lt;/li&gt;
&lt;li&gt;Voice Control: Children can navigate the board and place animals using only their voice.&lt;/li&gt;
&lt;li&gt;Haptic Logic: I integrated haptics to provide tactile vibrations for every move, allowing children to feel the logic of the game.&lt;/li&gt;
&lt;li&gt;Visual Clarity: Large touch targets and high-contrast color zones ensure the interface is easy to navigate for small hands.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;if-you-have-small-humans-in-your-life&quot;&gt;If you have small humans in your life&lt;/h4&gt;
&lt;p&gt;I'd love for you to try it. It's on the App Store, completely free, with no strings attached:&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/app/animal-sudoku-kids-puzzle/id6759012562&quot;&gt;Download Animal Sudoku on the App Store&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you give it a spin with a kid in your life, I genuinely want to hear how it goes — what worked, what didn't, what you wish it did. Reply here, send me a message, or email me directly. Every bit of feedback shapes what comes next.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Animal Sudoku Kids Puzzle is available now on iOS. Android version in development.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29236/why-i-built-animal-sudoku-without-a-single-ad</guid>
<pubDate>Wed, 30 Sep 2026 12:04:29 +0000</pubDate>
</item>
<item>
<title>New to Bitrise CodePush? Here's What I Wish I Knew Before Setting It Up</title>
<link>https://coderlegion.com/28183/new-to-bitrise-codepush-heres-what-i-wish-i-knew-before-setting-it-up</link>
<description>&lt;h2 id=&quot;new-to-bitrise-codepush-here-s-what-i-wish-i-knew-before-setting-it-up&quot;&gt;New to Bitrise CodePush? Here's What I Wish I Knew Before Setting It Up&lt;/h2&gt;
&lt;p&gt;If you've never used CodePush before, the idea sounds almost too convenient.&lt;/p&gt;
&lt;p&gt;You find a bug in a React Native app, fix the JavaScript, and instead of waiting for another App Store or Google Play release, you can push the update directly to users.&lt;/p&gt;
&lt;p&gt;That's the part that got my attention.&lt;/p&gt;
&lt;p&gt;But once I actually started setting it up, I realized there are quite a few things that aren't obvious from that one-line explanation.&lt;/p&gt;
&lt;p&gt;So if you're new to Bitrise CodePush, here's what I learned while going through the setup.&lt;/p&gt;
&lt;h3 id=&quot;first-what-does-codepush-actually-update-&quot;&gt;First: What Does CodePush Actually Update?&lt;/h3&gt;
&lt;p&gt;The easiest way to think about CodePush is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;It updates the JavaScript and assets in your React Native app without requiring a new store release.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So if you've fixed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A JavaScript bug&lt;/li&gt;
&lt;li&gt;Some text or copy&lt;/li&gt;
&lt;li&gt;A small UI issue&lt;/li&gt;
&lt;li&gt;Another change that doesn't require native code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can potentially deliver that fix without submitting a new binary to the App Store or Google Play.&lt;/p&gt;
&lt;p&gt;But there's an important boundary.&lt;/p&gt;
&lt;p&gt;If you change native code, CodePush doesn't magically bypass the app stores.&lt;/p&gt;
&lt;p&gt;You'll still need a normal app release.&lt;/p&gt;
&lt;p&gt;That's probably the first thing I'd make sure the team understands before using it.&lt;/p&gt;
&lt;h3 id=&quot;what-bitrise-actually-gives-you&quot;&gt;What Bitrise Actually Gives You&lt;/h3&gt;
&lt;p&gt;There are two main parts to the setup.&lt;/p&gt;
&lt;h4 id=&quot;1-codepush-inside-bitrise-release-management&quot;&gt;1. CodePush inside Bitrise Release Management&lt;/h4&gt;
&lt;p&gt;This is where you manage deployments, upload releases, and control things like rollout percentages.&lt;/p&gt;
&lt;h4 id=&quot;2-the-sdk-inside-your-application&quot;&gt;2. The SDK inside your application&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;@bitrise/code-push-sdk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SDK is responsible for checking whether an update is available and applying it.&lt;/p&gt;
&lt;p&gt;So the basic idea is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bitrise handles the release side → your app handles the update side.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Once I understood that split, the rest of the setup made a lot more sense.&lt;/p&gt;
&lt;h3 id=&quot;before-you-touch-the-code&quot;&gt;Before You Touch the Code&lt;/h3&gt;
&lt;p&gt;You'll need a Bitrise account and a Release Management app.&lt;/p&gt;
&lt;p&gt;One thing that caught my attention here is that Release Management and your normal Bitrise CI app aren't necessarily the same thing.&lt;/p&gt;
&lt;p&gt;If you're already using Bitrise CI to build your React Native app, Release Management is something you add alongside it.&lt;/p&gt;
&lt;p&gt;And if you're releasing for both iOS and Android, you'll need to account for both platforms in your setup.&lt;/p&gt;
&lt;p&gt;I'd recommend getting the basic Release Management setup working before touching the React Native project.&lt;/p&gt;
&lt;p&gt;It makes troubleshooting much easier.&lt;/p&gt;
&lt;h2 id=&quot;step-1-create-a-deployment&quot;&gt;Step 1: Create a Deployment&lt;/h2&gt;
&lt;p&gt;Inside the Release Management app, go to:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Configuration → Deployments → New deployment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For a real project, I'd probably start with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Staging&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Production&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That separation is useful.&lt;/p&gt;
&lt;p&gt;You don't want to be testing an update and accidentally send it to everyone using the production app.&lt;/p&gt;
&lt;p&gt;You can either provide an existing deployment key or let Bitrise generate one.&lt;/p&gt;
&lt;p&gt;One thing about deployment keys&lt;/p&gt;
&lt;p&gt;This initially confused me.&lt;/p&gt;
&lt;p&gt;The deployment key looks like something you'd normally hide in a secret.&lt;/p&gt;
&lt;p&gt;But it isn't really a password.&lt;/p&gt;
&lt;p&gt;The key eventually gets included in the application, which means someone with access to the built app could potentially extract it.&lt;/p&gt;
&lt;p&gt;It's better to think of it as an identifier for the deployment rather than a secret credential.&lt;/p&gt;
&lt;p&gt;Your API token is the thing you actually need to protect.&lt;/p&gt;
&lt;p&gt;Step 2: Install the SDK&lt;/p&gt;
&lt;p&gt;The first command is simple:&lt;/p&gt;
&lt;p&gt;npm install @bitrise/code-push-sdk&lt;/p&gt;
&lt;p&gt;After that, things depend on how your React Native app was created.&lt;/p&gt;
&lt;p&gt;And this is one place where I wouldn't recommend blindly copying configuration from a random tutorial.&lt;/p&gt;
&lt;p&gt;Expo and bare React Native aren't configured in exactly the same way.&lt;/p&gt;
&lt;p&gt;If You're Using Expo&lt;/p&gt;
&lt;p&gt;With Expo, you add the CodePush plugin to your app.json.&lt;/p&gt;
&lt;p&gt;You'll need your deployment key and the CodePush server URL.&lt;/p&gt;
&lt;p&gt;The URL follows this pattern:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;workspace-slug&amp;gt;.codepush.bitrise.io&quot;&gt;https://&amp;lt;workspace-slug&amp;gt;.codepush.bitrise.io&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;p&gt;npx expo prebuild&lt;/p&gt;
&lt;p&gt;I like doing this step because it lets you check whether the configuration actually made its way into the generated native project.&lt;/p&gt;
&lt;p&gt;If You're Using Bare React Native&lt;/p&gt;
&lt;p&gt;This takes a little more work.&lt;/p&gt;
&lt;p&gt;On iOS, you'll need to make a change in AppDelegate.swift so CodePush can resolve the active release bundle instead of always using the default bundled JavaScript.&lt;/p&gt;
&lt;p&gt;You'll also configure the deployment key and server URL in Info.plist.&lt;/p&gt;
&lt;p&gt;On Android, the configuration goes into strings.xml, along with some Gradle configuration and a change to how MainApplication.kt resolves the JavaScript bundle.&lt;/p&gt;
&lt;p&gt;None of these changes are particularly complicated.&lt;/p&gt;
&lt;p&gt;They're just easy places to make a small mistake.&lt;/p&gt;
&lt;p&gt;That's why I found it better to follow the Bitrise documentation closely rather than trying to recreate the setup from memory.&lt;/p&gt;
&lt;p&gt;Step 3: Think About the User Experience&lt;/p&gt;
&lt;p&gt;This was probably the part I didn't think about enough at first.&lt;/p&gt;
&lt;p&gt;It's easy to look at CodePush as:&lt;/p&gt;
&lt;p&gt;&quot;There's an update, download it.&quot;&lt;/p&gt;
&lt;p&gt;But you actually have choices about how that update reaches the user.&lt;/p&gt;
&lt;p&gt;For example, with:&lt;/p&gt;
&lt;p&gt;codePush.sync()&lt;/p&gt;
&lt;p&gt;you can use different behaviours.&lt;/p&gt;
&lt;p&gt;Silent update&lt;/p&gt;
&lt;p&gt;The update downloads in the background and is applied when the app restarts.&lt;/p&gt;
&lt;p&gt;For the user, the whole thing can be almost invisible.&lt;/p&gt;
&lt;p&gt;Update when the app resumes&lt;/p&gt;
&lt;p&gt;Instead of checking only when the app starts, the app can check when it comes back into the foreground.&lt;/p&gt;
&lt;p&gt;Interactive update&lt;/p&gt;
&lt;p&gt;The user gets feedback that an update is available and can be shown the download process.&lt;/p&gt;
&lt;p&gt;For mandatory updates, you can also prevent the user from simply dismissing them.&lt;/p&gt;
&lt;p&gt;You can even use progress callbacks if you want to show download progress.&lt;/p&gt;
&lt;p&gt;I think this is worth deciding before you ship your first update.&lt;/p&gt;
&lt;p&gt;The technically easiest option isn't necessarily the best experience for your users.&lt;/p&gt;
&lt;p&gt;Step 4: Build and Release&lt;/p&gt;
&lt;p&gt;Once the application is configured, you need to create the JavaScript bundle.&lt;/p&gt;
&lt;p&gt;For a bare React Native project:&lt;/p&gt;
&lt;p&gt;npx react-native bundle \&lt;br&gt;
  --platform ios \&lt;br&gt;
  --dev false \&lt;br&gt;
  --entry-file index.js \&lt;br&gt;
  --bundle-output ./build/main.jsbundle \&lt;br&gt;
  --assets-dest ./build&lt;/p&gt;
&lt;p&gt;For Expo:&lt;/p&gt;
&lt;p&gt;expo export:embed&lt;/p&gt;
&lt;p&gt;You then package the generated files and upload them to Release Management.&lt;/p&gt;
&lt;p&gt;You can do this manually from the dashboard, which is probably the easiest way to test your first release.&lt;/p&gt;
&lt;p&gt;Once you've confirmed everything works, that's when I'd start thinking about automation.&lt;/p&gt;
&lt;p&gt;Don't Immediately Send It to Everyone&lt;/p&gt;
&lt;p&gt;This is one feature I'd definitely use in production.&lt;/p&gt;
&lt;p&gt;When you create the release, you can control the rollout.&lt;/p&gt;
&lt;p&gt;You can target specific app versions and use version ranges such as:&lt;/p&gt;
&lt;p&gt;1.2.x&lt;/p&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;p&gt;&amp;gt;=1.2.3 &amp;lt;1.2.7&lt;/p&gt;
&lt;p&gt;You can also mark an update as mandatory and control the percentage of users who receive it.&lt;/p&gt;
&lt;p&gt;That last part is important.&lt;/p&gt;
&lt;p&gt;Imagine you've made a JavaScript change that you think is completely safe.&lt;/p&gt;
&lt;p&gt;Instead of immediately sending it to 100% of your users, you can start with a smaller percentage.&lt;/p&gt;
&lt;p&gt;If everything looks good, increase the rollout.&lt;/p&gt;
&lt;p&gt;That gives you a little more room to catch problems before they become everyone's problem.&lt;/p&gt;
&lt;p&gt;What About Automation?&lt;/p&gt;
&lt;p&gt;Once you've manually released a couple of updates, you'll probably notice the repetitive part:&lt;/p&gt;
&lt;p&gt;Build the bundle.&lt;br&gt;
Zip it.&lt;br&gt;
Upload it.&lt;br&gt;
Do it again for the other platform.&lt;/p&gt;
&lt;p&gt;It works.&lt;/p&gt;
&lt;p&gt;But I wouldn't want to do that every week.&lt;/p&gt;
&lt;p&gt;That's where Bitrise CI comes in.&lt;/p&gt;
&lt;p&gt;You can automate the build and upload process so that a workflow handles it for you.&lt;/p&gt;
&lt;p&gt;One approach I like is connecting the release to an approved and labelled pull request rather than automatically releasing every merge.&lt;/p&gt;
&lt;p&gt;That gives you a deliberate point where someone says:&lt;/p&gt;
&lt;p&gt;&quot;This is ready to ship.&quot;&lt;/p&gt;
&lt;p&gt;The CI system then takes care of the repetitive work.&lt;/p&gt;
&lt;p&gt;I wrote separately about that automation process because there are a few more details around credentials, versioning, and workflow configuration.&lt;/p&gt;
&lt;p&gt;My Checklist Before the First Release&lt;/p&gt;
&lt;p&gt;If I were setting this up on another project, this is the checklist I'd keep nearby:&lt;/p&gt;
&lt;p&gt;Bitrise Release Management is configured&lt;/p&gt;
&lt;p&gt;iOS and Android are accounted for&lt;/p&gt;
&lt;p&gt;Staging and Production deployments are created&lt;/p&gt;
&lt;p&gt;@bitrise/code-push-sdk is installed&lt;/p&gt;
&lt;p&gt;Expo or native React Native configuration is complete&lt;/p&gt;
&lt;p&gt;The app can successfully check for updates&lt;/p&gt;
&lt;p&gt;The sync behaviour has been chosen&lt;/p&gt;
&lt;p&gt;The first release is tested with a small rollout&lt;/p&gt;
&lt;p&gt;Production releases have an automation plan&lt;/p&gt;
&lt;p&gt;So, Is CodePush Worth It?&lt;/p&gt;
&lt;p&gt;I think it makes the most sense when you regularly have small JavaScript-level fixes that don't justify going through another full app-store release.&lt;/p&gt;
&lt;p&gt;It's not a replacement for your normal mobile release pipeline.&lt;/p&gt;
&lt;p&gt;Native changes still need a normal build and store submission.&lt;/p&gt;
&lt;p&gt;But if your team has ever had to submit a new mobile build because of something that could have been fixed entirely in JavaScript, that's the gap CodePush is trying to close.&lt;/p&gt;
&lt;p&gt;For me, the biggest takeaway wasn't just:&lt;/p&gt;
&lt;p&gt;&quot;I can push an update without the App Store.&quot;&lt;/p&gt;
&lt;p&gt;It was realizing that there are several decisions around how that update gets delivered, who receives it, and what the user experiences when it arrives.&lt;/p&gt;
&lt;p&gt;That's the part I'd pay attention to before putting CodePush into a production app.&lt;/p&gt;
&lt;p&gt;Have You Used CodePush?&lt;/p&gt;
&lt;p&gt;Have you used CodePush or another OTA update solution with React Native?&lt;/p&gt;
&lt;p&gt;I'd love to hear how you handled it, especially if you ran into problems during setup.&lt;/p&gt;
&lt;p&gt;What would you add to this checklist?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28183/new-to-bitrise-codepush-heres-what-i-wish-i-knew-before-setting-it-up</guid>
<pubDate>Tue, 22 Sep 2026 15:32:27 +0000</pubDate>
</item>
<item>
<title>Android System Back Button Closes the App After Upgrading to Target SDK 36 — React Native Fix</title>
<link>https://coderlegion.com/28094/android-system-back-button-closes-the-app-after-upgrading-to-target-sdk-36-react-native-fix</link>
<description>&lt;p&gt;If you're maintaining a React Native Android application and recently upgraded your project to &lt;strong&gt;target SDK 36 (Android 16)&lt;/strong&gt;, you may encounter an unexpected navigation issue:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Pressing the Android system back button closes the app instead of navigating to the previous screen.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This can be particularly confusing when React Navigation is already configured correctly and the same navigation flow works perfectly on older Android target SDK versions.&lt;/p&gt;
&lt;p&gt;This article explains what changed, how to identify the issue, the workaround I used, and what you should consider for a proper Android 16 migration.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;After upgrading the Android project from an older target SDK to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gradle&quot;&gt;targetSdkVersion = 36
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the app started behaving differently when using the &lt;strong&gt;Android system back button&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For example, suppose the navigation stack is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
  ↓
Products
  ↓
Product Details
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the user presses the Android system back button from &lt;strong&gt;Product Details&lt;/strong&gt;, the expected behavior is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Product Details
        ↓
    Products
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead, the application could close.&lt;/p&gt;
&lt;p&gt;Interestingly, navigation using React Navigation itself continued to work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;navigation.goBack();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The issue was specifically related to the &lt;strong&gt;Android system back event&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;environment&quot;&gt;Environment&lt;/h2&gt;
&lt;p&gt;The issue was observed in a React Native application with a setup similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React Native
React Navigation
Android SDK 36
Target SDK 36
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important change was upgrading the Android target SDK to &lt;strong&gt;API level 36&lt;/strong&gt;, corresponding to Android 16.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-does-this-happen-&quot;&gt;Why Does This Happen?&lt;/h2&gt;
&lt;p&gt;The important part of the migration is the Android back-navigation change.&lt;/p&gt;
&lt;p&gt;Android has been moving away from the older:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;onBackPressed()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mechanism toward the newer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;OnBackInvokedDispatcher
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and predictive back navigation APIs.&lt;/p&gt;
&lt;p&gt;With newer Android versions and target SDK requirements, applications need to properly participate in the newer back-navigation system.&lt;/p&gt;
&lt;p&gt;This can expose compatibility issues in applications or libraries that still rely on the older back handling mechanism.&lt;/p&gt;
&lt;p&gt;In a React Native application, the Android system back event eventually needs to be correctly propagated into the React Native / React Navigation layer.&lt;/p&gt;
&lt;p&gt;If that doesn't happen, Android may interpret the back action as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No navigation handler
        ↓
Finish current Activity
        ↓
App closes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;System Back
     ↓
React Native Back Handler
     ↓
React Navigation
     ↓
Previous screen
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-i-identified-the-problem&quot;&gt;How I Identified the Problem&lt;/h2&gt;
&lt;p&gt;The first thing I checked was whether React Navigation itself was working.&lt;/p&gt;
&lt;p&gt;Navigation from the application was working correctly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;navigation.navigate(&quot;ProductDetails&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and programmatic back navigation also worked:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;navigation.goBack();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem only occurred when pressing the &lt;strong&gt;physical/system Android back button&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I also checked whether the application was closing directly from the Home screen.&lt;/p&gt;
&lt;p&gt;That behavior is expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
 ↓
Android Back
 ↓
App exits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual problem was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
 ↓
Product List
 ↓
Product Details
 ↓
Android Back
 ↓
App exits ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
 ↓
Product List
 ↓
Product Details
 ↓
Android Back
 ↓
Product List ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This distinction is important when debugging Android back navigation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-androidmanifest-configuration&quot;&gt;The AndroidManifest Configuration&lt;/h2&gt;
&lt;p&gt;The Android application can control the behavior of the newer back system using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;android:enableOnBackInvokedCallback
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;application
    android:name=&quot;.MainApplication&quot;
    android:label=&quot;@string/app_name&quot;
    android:icon=&quot;@mipmap/ic_launcher&quot;
    android:enableOnBackInvokedCallback=&quot;false&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my case, setting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;android:enableOnBackInvokedCallback=&quot;false&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;worked as a temporary workaround.&lt;/p&gt;
&lt;p&gt;After adding this configuration, the Android system back button once again started navigating to the previous React Navigation screen instead of immediately closing the application.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-does-false-work-&quot;&gt;Why Does &lt;code&gt;false&lt;/code&gt; Work?&lt;/h2&gt;
&lt;p&gt;Setting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;android:enableOnBackInvokedCallback=&quot;false&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;essentially disables the application's participation in the newer &lt;code&gt;OnBackInvokedDispatcher&lt;/code&gt; behavior.&lt;/p&gt;
&lt;p&gt;This allows the application to continue using the older back handling path.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;h4 id=&quot;without-the-workaround&quot;&gt;Without the workaround&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Android System Back
        ↓
OnBackInvokedDispatcher
        ↓
React Native / Navigation handler
        ↓
❌ Handler not properly triggered
        ↓
Activity finishes
        ↓
App closes
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;with-the-temporary-workaround&quot;&gt;With the temporary workaround&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Android System Back
        ↓
Legacy back handling
        ↓
React Native BackHandler
        ↓
React Navigation
        ↓
Previous screen
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the workaround doesn't really &quot;fix&quot; predictive back support.&lt;/p&gt;
&lt;p&gt;It tells Android to avoid the newer callback behavior for this application.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;is-enableonbackinvokedcallback-false-the-permanent-fix-&quot;&gt;Is &lt;code&gt;enableOnBackInvokedCallback=&quot;false&quot;&lt;/code&gt; the Permanent Fix?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Not necessarily.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is an important distinction.&lt;/p&gt;
&lt;p&gt;If your application is targeting Android 16, you should ideally investigate whether your React Native version and navigation dependencies properly support Android's newer back-navigation behavior.&lt;/p&gt;
&lt;p&gt;Using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;android:enableOnBackInvokedCallback=&quot;false&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;can be useful as a compatibility workaround while migrating, but it should not automatically be considered the final Android 16 solution.&lt;/p&gt;
&lt;p&gt;The long-term goal should be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Android 16
      ↓
Predictive Back
      ↓
OnBackInvokedDispatcher
      ↓
React Native
      ↓
React Navigation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;working correctly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;check-your-react-native-version&quot;&gt;Check Your React Native Version&lt;/h2&gt;
&lt;p&gt;Before adding application-level workarounds, check the React Native version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm list react-native
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yarn why react-native
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also check React Navigation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm list @react-navigation/native
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;react-native
@react-navigation/native
@react-navigation/native-stack
@react-navigation/bottom-tabs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Older combinations may have different levels of Android predictive-back support.&lt;/p&gt;
&lt;p&gt;It's important to evaluate the complete dependency chain rather than changing only the AndroidManifest.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;check-your-mainactivity&quot;&gt;Check Your &lt;code&gt;MainActivity&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Also inspect your Android &lt;code&gt;MainActivity&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A typical React Native application may have something similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MainActivity extends ReactActivity {

    @Override
    protected String getMainComponentName() {
        return &quot;MyApp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your application has custom back handling, search the Android project for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;onBackPressed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -R &quot;onBackPressed&quot; android/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also search for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;OnBackInvokedDispatcher
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;OnBackInvokedCallback
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helps identify whether custom native code is interfering with the new Android back-navigation mechanism.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;check-react-native-backhandler&quot;&gt;Check React Native &lt;code&gt;BackHandler&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;React Native applications can listen for Android back events using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { BackHandler } from &quot;react-native&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  const subscription = BackHandler.addEventListener(
    &quot;hardwareBackPress&quot;,
    () =&amp;gt; {
      navigation.goBack();
      return true;
    }
  );

  return () =&amp;gt; subscription.remove();
}, [navigation]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, &lt;strong&gt;don't add this globally just to hide the problem&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;React Navigation normally handles Android back navigation for its navigation stack.&lt;/p&gt;
&lt;p&gt;Adding multiple global &lt;code&gt;BackHandler&lt;/code&gt; listeners can introduce new problems such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Back event being consumed too early&lt;/li&gt;
&lt;li&gt;Incorrect navigation&lt;/li&gt;
&lt;li&gt;Screens being popped multiple times&lt;/li&gt;
&lt;li&gt;Modals not closing correctly&lt;/li&gt;
&lt;li&gt;Navigation stack becoming inconsistent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So first determine whether the underlying Android back event is reaching React Native correctly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;a-useful-debugging-test&quot;&gt;A Useful Debugging Test&lt;/h2&gt;
&lt;p&gt;You can temporarily add a &lt;code&gt;BackHandler&lt;/code&gt; listener to determine whether React Native receives the system back event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  const subscription = BackHandler.addEventListener(
    &quot;hardwareBackPress&quot;,
    () =&amp;gt; {
      console.log(&quot;ANDROID BACK EVENT RECEIVED&quot;);

      return false;
    }
  );

  return () =&amp;gt; subscription.remove();
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then navigate to a screen and press the Android system back button.&lt;/p&gt;
&lt;p&gt;If you see:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ANDROID BACK EVENT RECEIVED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then the event is reaching React Native.&lt;/p&gt;
&lt;p&gt;If the app closes without the event reaching your JavaScript handler, the problem is likely further down in the Android back-dispatching path.&lt;/p&gt;
&lt;p&gt;This is a useful way to separate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Android back event problem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React Navigation configuration problem
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;don-t-confuse-home-screen-behavior-with-a-bug&quot;&gt;Don't Confuse Home Screen Behavior With a Bug&lt;/h2&gt;
&lt;p&gt;One important testing detail:&lt;/p&gt;
&lt;p&gt;If the user is already on the root screen:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pressing Android Back is normally expected to exit the application.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
 ↓
Android Back
 ↓
App closes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That alone isn't evidence of a problem.&lt;/p&gt;
&lt;p&gt;The important test is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
 ↓
Screen A
 ↓
Screen B
 ↓
Android Back
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Screen B
 ↓
Screen A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Screen B
 ↓
App closes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then back navigation is not being handled correctly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;testing-checklist-after-target-sdk-36-upgrade&quot;&gt;Testing Checklist After Target SDK 36 Upgrade&lt;/h2&gt;
&lt;p&gt;After upgrading to API 36, I recommend testing all of these scenarios.&lt;/p&gt;
&lt;h4 id=&quot;1-normal-navigation&quot;&gt;1. Normal navigation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home → List → Details
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Press system Back.&lt;/p&gt;
&lt;p&gt;Expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Details → List
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-multiple-navigation-levels&quot;&gt;2. Multiple navigation levels&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home → A → B → C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Press Back repeatedly.&lt;/p&gt;
&lt;p&gt;Expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C → B → A → Home
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-root-screen&quot;&gt;3. Root screen&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Home
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Press Back.&lt;/p&gt;
&lt;p&gt;Expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;App exits
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-modal&quot;&gt;4. Modal&lt;/h4&gt;
&lt;p&gt;Open a modal and press Back.&lt;/p&gt;
&lt;p&gt;Expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Modal closes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;App exits
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-bottom-tabs&quot;&gt;5. Bottom tabs&lt;/h4&gt;
&lt;p&gt;Test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Tab A → Tab B → Screen
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and verify that system Back behaves consistently with your navigation design.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;6-authentication-flow&quot;&gt;6. Authentication flow&lt;/h4&gt;
&lt;p&gt;Test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Login → OTP → Home
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and make sure Back doesn't allow users to incorrectly return to authentication screens.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;7-deep-links&quot;&gt;7. Deep links&lt;/h4&gt;
&lt;p&gt;Test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Deep Link → Details
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and verify the back stack behaves as expected.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;8-android-gesture-navigation&quot;&gt;8. Android gesture navigation&lt;/h4&gt;
&lt;p&gt;Test using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3-button navigation&lt;/li&gt;
&lt;li&gt;Gesture navigation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Predictive back behavior can differ from traditional button-based testing, so both should be tested.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;temporary-workaround&quot;&gt;Temporary Workaround&lt;/h2&gt;
&lt;p&gt;If your application is currently affected and you need to stabilize the release, one possible temporary configuration is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;application
    ...
    android:enableOnBackInvokedCallback=&quot;false&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After rebuilding the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd android
./gradlew clean
cd ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then rebuild:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx react-native run-android
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or use your normal release build process.&lt;/p&gt;
&lt;p&gt;For an Expo bare/prebuild project, make sure the generated Android configuration is also correctly reflected in the source configuration you use for builds, rather than relying on a manual change that can be overwritten.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;recommended-migration-approach&quot;&gt;Recommended Migration Approach&lt;/h2&gt;
&lt;p&gt;Instead of treating the manifest flag as the final solution, I recommend approaching the migration in this order:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Upgrade target SDK → 36
          ↓
2. Test system Back
          ↓
3. Check React Native version
          ↓
4. Check React Navigation versions
          ↓
5. Search for custom Android back handling
          ↓
6. Search for BackHandler overrides
          ↓
7. Test predictive back
          ↓
8. Update incompatible dependencies
          ↓
9. Use manifest workaround if required
          ↓
10. Remove workaround after proper migration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach makes it easier to identify the actual compatibility problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;important-takeaway&quot;&gt;Important Takeaway&lt;/h2&gt;
&lt;p&gt;Upgrading:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;targetSdkVersion
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is not always just a build configuration change.&lt;/p&gt;
&lt;p&gt;A target SDK upgrade can activate or expose new Android platform behavior.&lt;/p&gt;
&lt;p&gt;For Android 16 / API 36, &lt;strong&gt;back navigation and predictive back behavior are especially important areas to test&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If your React Native app suddenly starts closing when the Android system back button is pressed after moving to target SDK 36, don't immediately assume that your React Navigation stack is broken.&lt;/p&gt;
&lt;p&gt;First determine whether:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Android
   ↓
Back Dispatcher
   ↓
React Native
   ↓
React Navigation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is working correctly.&lt;/p&gt;
&lt;p&gt;The following configuration can serve as a temporary compatibility workaround:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;android:enableOnBackInvokedCallback=&quot;false&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but the preferred long-term solution is to ensure that the React Native and navigation stack properly supports Android's modern back-navigation APIs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-checklist&quot;&gt;Final Checklist&lt;/h2&gt;
&lt;p&gt;Before releasing a target SDK 36 build, verify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Android system Back navigates correctly&lt;/li&gt;
&lt;li&gt;[ ] Root screen exits correctly&lt;/li&gt;
&lt;li&gt;[ ] Nested navigation works&lt;/li&gt;
&lt;li&gt;[ ] Modals handle Back correctly&lt;/li&gt;
&lt;li&gt;[ ] Bottom tabs behave correctly&lt;/li&gt;
&lt;li&gt;[ ] Authentication screens behave correctly&lt;/li&gt;
&lt;li&gt;[ ] Deep links create the expected back stack&lt;/li&gt;
&lt;li&gt;[ ] Gesture navigation works&lt;/li&gt;
&lt;li&gt;[ ] 3-button navigation works&lt;/li&gt;
&lt;li&gt;[ ] Predictive back behavior has been tested&lt;/li&gt;
&lt;li&gt;[ ] React Native version is compatible&lt;/li&gt;
&lt;li&gt;[ ] React Navigation dependencies are compatible&lt;/li&gt;
&lt;li&gt;[ ] Custom &lt;code&gt;BackHandler&lt;/code&gt; logic has been reviewed&lt;/li&gt;
&lt;li&gt;[ ] Native &lt;code&gt;onBackPressed()&lt;/code&gt; implementations have been reviewed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;OnBackInvokedDispatcher&lt;/code&gt; usage has been reviewed&lt;/li&gt;
&lt;li&gt;[ ] Any temporary &lt;code&gt;enableOnBackInvokedCallback&lt;/code&gt; workaround is documented&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Android 16 introduces another step in the evolution of Android back navigation. For React Native applications, upgrading to &lt;strong&gt;target SDK 36&lt;/strong&gt; can expose problems where the system Back button no longer reaches the JavaScript navigation layer as expected.&lt;/p&gt;
&lt;p&gt;If your app starts closing instead of navigating backward, investigate the new Android back-dispatching behavior before modifying your navigation stack.&lt;/p&gt;
&lt;p&gt;A manifest-level workaround such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;android:enableOnBackInvokedCallback=&quot;false&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;may restore the previous behavior temporarily, but dependency compatibility and proper predictive-back support should be addressed as part of the complete migration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Target SDK upgrades should always be followed by dedicated navigation regression testing—not just build and installation testing.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;✍️ Written by &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/dainyjose&quot;&gt;&lt;strong&gt;Dainy Jose&lt;/strong&gt;&lt;/a&gt; — React Native Mobile Application Developer with 3+ years of experience building cross-platform mobile apps using &lt;strong&gt;React Native (Expo, TypeScript, Redux)&lt;/strong&gt;. &lt;br&gt;
Currently expanding backend knowledge through the &lt;strong&gt;MERN Stack (MongoDB, Express.js, React.js, Node.js)&lt;/strong&gt; to create more efficient, full-stack mobile experiences.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Tech Stack:&lt;/strong&gt; React Native · TypeScript · Redux · Expo · Firebase · Node.js · Express.js · MongoDB · REST API · JWT · Jest · Google Maps · Razorpay · PayU · Agile · SDLC · Git · Bitbucket · Jira &lt;/p&gt;
&lt;h2 id=&quot;section&quot;&gt;&lt;/h2&gt;
&lt;p&gt; &lt;strong&gt;Connect with me&lt;/strong&gt;:&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://dainyjose.github.io/my-portfolio/&quot;&gt;Portfolio&lt;/a&gt;&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/dainyjose&quot;&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dainyjose&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28094/android-system-back-button-closes-the-app-after-upgrading-to-target-sdk-36-react-native-fix</guid>
<pubDate>Tue, 22 Sep 2026 04:30:24 +0000</pubDate>
</item>
<item>
<title>Wire AI Is Live: Beta tester open</title>
<link>https://coderlegion.com/23855/wire-ai-is-live-beta-tester-open</link>
<description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Wire AI is live and early access is open. It's an AI-native growth engineer for mobile apps: your React Native app embeds an SDK, a LangGraph backend decides what to ask each user next, and deterministic code owns the routing while the model only generates content. On one live app, activation went from 69% to 93%. 15 early-access seats, 3 taken, free and installed by hand.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This article is part of my Code Meet AI newsletter: &lt;a rel=&quot;nofollow&quot; href=&quot;https://codemeetai.substack.com&quot;&gt;https://codemeetai.substack.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Wire AI is live today, and early access is open.&lt;/p&gt;
&lt;p&gt;It's the AI-native growth engineer for mobile apps: AI that replaces your growth team. It runs four versions of every step of your users' journey, live at once, and keeps what makes them stay.&lt;/p&gt;
&lt;p&gt;On the first app it ran on, activation went from 69% to 93%.&lt;/p&gt;
&lt;p&gt;15 seats. 3 are already gone. Free, installed by me personally, and there's no price attached because I haven't set one yet.&lt;/p&gt;
&lt;p&gt;Below: what it does, what already shipped, what's still rough, and how to claim one of the 12 that are left.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How it works&lt;/h3&gt;
&lt;p&gt;Your users' journey has stages. They onboard, they get a plan, they hit their first real win, they get asked for a review. Wire AI runs four versions of each of those stages, live, at the same time. Users are assigned to a version and stay in it, the funnel says which version keeps more of them, and the winner gets promoted.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substack-post-media.s3.amazonaws.com/public/images/0c520c45-4107-426f-94b6-9932da658a28_2400x1560.png&quot; alt=&quot;The learning loop: four versions of every journey stage, the winner promoted, every finished journey feeding the next round.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then the part a testing tool doesn't do. Every finished journey feeds back in: Wire reads what happened and writes a sharper variant for the next round.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;It isn't a dashboard that tells you to go run a test. It's the loop, already running.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-that-looks-like-in-your-codebase&quot;&gt;What that looks like in your codebase&lt;/h3&gt;
&lt;p&gt;Your React Native app embeds a small SDK. When a user opens the app, the SDK talks to a backend I run, and that backend decides, one screen at a time, what to ask that specific person next. It renders as native cards. Not a webview, not a chat window bolted onto your product.&lt;/p&gt;
&lt;p&gt;At the end it hands your app a structured plan you can save and act on, and every step streams into a funnel you can read.&lt;/p&gt;
&lt;p&gt;So instead of writing an onboarding once and guessing for the next two years, you write a config and the AI runs it.&lt;/p&gt;
&lt;p&gt;The part that makes it work is boring on purpose. Deterministic code controls the flow and the model only generates content. Routing is a pure function of where the session is, how many questions are left, whether the user is bailing, so the whole flow is testable without a model in the loop.&lt;/p&gt;
&lt;p&gt;The AI writes the next question, grounded in every answer the person already gave, and writes the final plan. Everything it emits gets validated against a fixed vocabulary of components before it's allowed anywhere near a screen.&lt;/p&gt;
&lt;p&gt;That's the difference between a generative UI demo and something you can put in front of paying users. Most AI UI demos let the model draw whatever it wants and hope. This one gives the model a small, checked job and keeps the state machine in code where it belongs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substack-post-media.s3.amazonaws.com/public/images/7f7524d4-150f-4163-952a-f6b80a100b78_2400x1500.png&quot; alt=&quot;How a single turn runs, from the app through A2A to a schema-checked card.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-that-gets-you&quot;&gt;What that gets you&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A journey that adapts per user.&lt;/strong&gt; The static onboarding asks everybody the same seven questions in the same order. Wire AI asks what follows from what the person just said, stops when it has enough, and skips what it can already infer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Experiments that run without you.&lt;/strong&gt; A/B/C/D variants across the journey, five stages, four arms, twenty combinations, without anybody sitting in a meeting deciding which to try next. This is the direction the product is built toward and I'll be straight about where it sits today, further down.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A funnel you can actually read.&lt;/strong&gt; Completion, drop by step, time per screen, and the transcript of any individual session so you can read exactly what the AI asked and what the person answered. Most apps at this stage are guessing about where users give up.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You find out who your users actually are.&lt;/strong&gt; Wire AI reads every session and hands you the patterns: what people came for, which question makes them hesitate, which wording converts. Most founders are still running on a persona doc written before launch.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;our-pilot-client-and-what-actually-happened&quot;&gt;Our pilot client, and what actually happened&lt;/h3&gt;
&lt;p&gt;I can't name the app, that's a client boundary I hold. What I can tell you is what it does: it helps people find spots to hang out. Live React Native app, real users, and an onboarding people were quietly dropping out of.&lt;/p&gt;
&lt;p&gt;Here's the run, in the order it happened.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;We started with an experiment, not a redesign.&lt;/strong&gt; Three versions of the onboarding running live at the same time, users assigned stickily so nobody got a different flow on their second open, every version chasing the same goal event. Nobody sat in a room deciding which one was better.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The first read was wrong, and the loop is what caught it.&lt;/strong&gt; One arm came back looking like a 17 point winner. At around fifty sessions that feels like a result, and it's exactly the moment a human growth team ships it and builds the next quarter on top. At scale it was worth about three and a half points. Mostly noise wearing a nice hat.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The version that actually won was the short one.&lt;/strong&gt; The deep, thorough discovery flow lost to a fast one of about eight screens. On a flow with no real leak in it, where every individual question converts in the high nineties, length turned out to be the only lever that moved anything.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Then the part I didn't expect: we found out who the users actually were.&lt;/strong&gt; The product context we had written assumed one kind of person, someone new to the city looking for places. The answers said something else. Younger, local, born there, social, budget conscious. My actual reaction, reading the answers: wait, these people already live here. We had spent months onboarding a user who didn't exist.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;So we rewrote the messaging to the real one.&lt;/strong&gt; New product context, questions rewritten for the person who actually shows up, one question cut completely because it was seasonal noise nobody cared about, and a guard added so the AI stops asking the same thing twice in different words.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Three learning loops in, activation had gone from 69% to 93%&lt;/strong&gt;, and the first movement showed up inside the first week of running live.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Two honest caveats on that number, because it's the one people will quote. It's activation on one app, in one category, over one window, so it isn't a portfolio of case studies and I won't dress it up as one.&lt;/p&gt;
&lt;p&gt;And the winning arm's completion rate moved by about thirteen points between generations of the experiment, which is a reminder that you read these numbers per run, never all-time. That lesson cost me a re-read of my own dashboard.&lt;/p&gt;
&lt;h3 id=&quot;what-already-shipped&quot;&gt;What already shipped&lt;/h3&gt;
&lt;p&gt;Not a landing page and a waitlist. These pieces exist and run.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The SDK is open source.&lt;/strong&gt; &lt;code&gt;wireai-rn&lt;/code&gt; on npm, MIT licensed, eleven built in components with validated schemas, six adapters for different model providers. Swapping providers is one environment variable, so you're not married to whoever is winning this quarter.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The transport is an open standard.&lt;/strong&gt; The device and the backend speak Google's A2A over JSON-RPC 2.0, the same contract implemented on both ends. That's how a Python agent graph ends up driving a native React Native screen. One call, one turn, one card.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The backend is a real service.&lt;/strong&gt; Python, FastAPI, LangGraph. Multi tenant, per app keys with scoped access, session state living outside the process so it scales sideways, data in &lt;code&gt;eu-central-1&lt;/code&gt; because GDPR isn't a Q3 problem.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;There's a console.&lt;/strong&gt; Register an app, edit the config without cutting a release, read the funnel, open any session and read the transcript.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's tested like something people depend on.&lt;/strong&gt; The drop in kit runs 630 tests on every merge, and the agent routing has its own eval suite with an accuracy gate, so &quot;the agent got worse&quot; shows up as a failing build instead of a feeling.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;First card lands in about 130 milliseconds, against roughly three and a half seconds when I started. Onboarding is the one screen where a spinner costs you the user permanently.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-a-seat-gets-you&quot;&gt;What a seat gets you&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;If Wire AI doesn't lift your activation, you've paid nothing and you still keep the teardown.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's the deal in one sentence. Here's everything inside it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Activation Teardown, before you commit to anything.&lt;/strong&gt; I put your existing onboarding through the analysis and send you a report: where people drop, which step leaks, what I would change first and why. It's yours whether you take a seat or walk away, and you don't need to install a single line of code to get it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The install, done by me, by hand.&lt;/strong&gt; Up to four hours on a call, in your codebase, together. Not a docs link and good luck. This is the part that doesn't scale, and it's exactly why there are 15 seats and not 500.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your bugs jump the queue.&lt;/strong&gt; When you hit something broken, and you will, it gets fixed that day instead of queueing behind everything else. That stops being possible the moment this cohort is full.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A direct line to the roadmap.&lt;/strong&gt; What gets built next is what early-access apps need next. At this stage that isn't landing-page language, it's just how the queue works, because there is nobody in front of you.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analytics free to 1 million events a month.&lt;/strong&gt; Any app, not only early access, and it doesn't expire when the beta does.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No price, and that's deliberate. I'm not deciding what Wire AI costs until early access tells me what it's worth, so nobody in these 15 seats gets priced by a guess.&lt;/p&gt;
&lt;h3 id=&quot;what-i-want-back&quot;&gt;What I want back&lt;/h3&gt;
&lt;p&gt;One thing: the truth, while you use it.&lt;/p&gt;
&lt;p&gt;When a question the AI asks is stupid, say so. When the console confuses you, say so. When a number looks wrong, say so, and that one is worth more to me than any testimonial you could write.&lt;/p&gt;
&lt;p&gt;That's the whole trade. You get the teardown, the install, the queue and the roadmap line. I get the thing I can't buy, which is honest reactions from a real app with real users in it.&lt;/p&gt;
&lt;h3 id=&quot;if-it-isn-t-you-it-might-be-someone-you-know&quot;&gt;If it isn't you, it might be someone you know&lt;/h3&gt;
&lt;p&gt;All three seats that are gone came warm. Somebody knew somebody. Not one came from a cold email, and I have sent a lot of cold emails, so I've stopped pretending that's the channel.&lt;/p&gt;
&lt;p&gt;So if your own app doesn't fit but a name came to mind while you were reading, that's the ask. Forward this to them, or drop the name in a comment and I'll reach out and tell them you sent me.&lt;/p&gt;
&lt;p&gt;Anyone arriving through a referral skips the queue and goes straight to a call. And I'll run the Activation Teardown on your app too, even if you never install anything.&lt;/p&gt;
&lt;h3 id=&quot;before-you-say-yes-two-things-you-should-know&quot;&gt;Before you say yes, two things you should know&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Only the onboarding stage is live and proven.&lt;/strong&gt; The journey wide experimentation I described up there is the roadmap, not a shipped feature. I'd rather you hear that now than find it out in week two.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I have an open number I can't explain yet.&lt;/strong&gt; This week my own dashboard read 23% completion on an app that had been finishing north of ninety. I spent a morning on it and I still don't have a clean answer. Best guess is the dashboard, there are two honest ways to compute a completion rate and my headline tile looks like it's reading the wrong one, but I haven't proven it and I'm not going to pretend I have.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both of those are in a launch post on purpose. The product is deliberately incomplete at this stage, and the fastest way to harden it is with a few real apps inside it rather than a year of me polishing it alone. If that reads as a red flag, this isn't the right moment for you and that's a fair call.&lt;/p&gt;
&lt;h3 id=&quot;who-this-is-for&quot;&gt;Who this is for&lt;/h3&gt;
&lt;p&gt;You're a fit if you have a React Native or Expo app that's live, with real users going through an onboarding right now, and you're a solo founder or a tiny team where the person who feels the activation problem is also the person who decides.&lt;/p&gt;
&lt;p&gt;It helps if your app already has AI or personalization in it, because then I don't have to sell you the idea and we can go straight to the mechanics.&lt;/p&gt;
&lt;p&gt;You're not a fit if you already have a growth team running experiments every week. Genuinely. A larger company passed on this recently for exactly that reason and they were right to. The value here is removing a person from a loop you don't have. If you have the person, you don't need me.&lt;/p&gt;
&lt;p&gt;Pre launch is also a bad fit, and I say that with some regret because those are usually the most fun conversations. Without traffic there's nothing for the loop to learn from.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-to-claim-a-seat&quot;&gt;How to claim a seat&lt;/h3&gt;
&lt;p&gt;Reply to this email. That's the whole process, there's no form and no demo request.&lt;/p&gt;
&lt;p&gt;Tell me what your app does and roughly how many people go through onboarding in a week. Two sentences is plenty. If it's a fit I'll send you a time this week. If it isn't, I'll tell you straight instead of quietly moving you into a nurture sequence.&lt;/p&gt;
&lt;p&gt;Not sure you qualify? Ask for the teardown anyway. Worst case, you get a report on your own funnel and we both move on.&lt;/p&gt;
&lt;p&gt;12 seats left of 15. The cap is real, because the install is manual and I'm the one doing it.&lt;/p&gt;
&lt;h3 id=&quot;how-i-make-this&quot;&gt;How I make this&lt;/h3&gt;
&lt;p&gt;I draft with AI. I've written about my workflow more than once and it'd be strange to hide it here. The receipts, the numbers, the decisions and the opinions are mine, the sentences get a drafting partner and then a lot of editing from me. If that disqualifies me for you, fair enough. If it interests you, that's most of what this newsletter is about.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I'm Malik, 9 years building mobile apps, React Native and Expo. I write Code Meet AI, where I show what I'm building with AI and what it costs me. Wire AI lives at &lt;a rel=&quot;nofollow&quot; href=&quot;https://getwireai.com&quot;&gt;getwireai.com&lt;/a&gt;, and the SDK is open source.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23855/wire-ai-is-live-beta-tester-open</guid>
<pubDate>Fri, 31 Jul 2026 11:29:54 +0000</pubDate>
</item>
<item>
<title>Lioric: Recent Updates.</title>
<link>https://coderlegion.com/19890/lioric-recent-updates</link>
<description>&lt;p&gt;Hello Developers!&lt;/p&gt;
&lt;p&gt;Recently, I released a new &lt;strong&gt;Lioric&lt;/strong&gt; version &lt;code&gt;0.1.9&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now our &lt;strong&gt;Lioric&lt;/strong&gt; Widget is completely customizable according to your website UI.&lt;/p&gt;
&lt;p&gt;Changes happened in this update:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Now the model props in the Lioric tag are optional; you can just leave those props, our widget automatically takes the model name as &quot;gemini-3-flash-preview.&quot; In the future, if we introduce any other models, you can just specify that model using the prop name as &quot;model&quot; as before.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Another major update about Liroic Widget, Now the Liroic Widget is completely customizable, starting from the widget logo to all the colours and Assistant name etc. Now the user can update the colors and the logo according to their website UI.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Updated Tag :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;Lioric
      apiKey=&quot;lioric_live_...&quot;
      llmApiKey=&quot;your_gemini_api_key&quot;
      assistantName=&quot;your_assisstant_name&quot;
      logoSrc=&quot;/brand-bot.gif&quot;
      logoAlt=&quot;Lioric&quot;
      theme={{
        accentColor: &quot;#2563eb&quot;,
        launcherBackground: &quot;#111827&quot;,
        userMessageBackground: &quot;#2563eb&quot;,
        assistantMessageBackground: &quot;#f8fafc&quot;,
        assistantMessageTextColor: &quot;#0f172a&quot;
      }}
      position=&quot;bottom-left&quot;
    /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The theme prop is completely optional. If you don't want the customized colors, you can just remove them and use the default colors. same with logoAlt and logoSrc as well. All the customizations are not necessary.&lt;/p&gt;
&lt;p&gt;That's all, Thanks!&lt;br&gt;
Stay tuned for future updates with me...&lt;/p&gt;
&lt;p&gt;Author/Builder : &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/VinnuGollakoti&quot;&gt;Vinnugollakoti&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19890/lioric-recent-updates</guid>
<pubDate>Sun, 07 Jun 2026 08:02:48 +0000</pubDate>
</item>
<item>
<title>Building LifeFast: A Solo Founder's Deep Dive into Fasting App Architecture</title>
<link>https://coderlegion.com/19608/building-lifefast-a-solo-founders-deep-dive-into-fasting-app-architecture</link>
<description>&lt;hr&gt;
&lt;p&gt;title: Building LifeFast: A Solo Founder's Deep Dive into Fasting App Architecture&lt;br&gt;
published: true&lt;/p&gt;
&lt;h3 id=&quot;tags-reactnative-postgres-expo-healthapp&quot;&gt;tags: reactnative, postgres, expo, healthapp&lt;/h3&gt;
&lt;p&gt;There's a specific kind of frustration that comes from using a health app that feels like it was built by engineers who've never actually used it.&lt;/p&gt;
&lt;p&gt;The timer shows a number. The number counts down. You earn a badge. That's it.&lt;/p&gt;
&lt;p&gt;No context about what's happening inside your body. No sense of progression. No genuine reason to keep going beyond the guilt of breaking a streak. Just a countdown and a badge that means nothing by noon.&lt;/p&gt;
&lt;p&gt;That frustration is what made me build &lt;a rel=&quot;nofollow&quot; href=&quot;https://lifefast.online&quot;&gt;LifeFast&lt;/a&gt; — a fasting tracker that treats users like intelligent adults who deserve to understand their own biology.&lt;/p&gt;
&lt;p&gt;This is the story of how I built it. The technical decisions, the real challenges, the things I got completely wrong, and what I'd do differently. If you're building a health app, a mobile product, or just a solo-founder side project trying to become something real — I hope this saves you some time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-stack-and-why-i-made-these-choices-&quot;&gt;The Stack (And Why I Made These Choices)&lt;/h3&gt;
&lt;p&gt;Let me get the boring part out of the way first, because the choices here shaped everything downstream.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; React Native via Expo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Node.js + Express + TypeScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State management:&lt;/strong&gt; Redux Toolkit + RTK Query&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build:&lt;/strong&gt; EAS Build (Expo Application Services)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email:&lt;/strong&gt; Resend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payments:&lt;/strong&gt; Google Play Billing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push notifications:&lt;/strong&gt; Expo Push + custom job queue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I picked React Native over Flutter primarily because I'm a JavaScript developer at heart and the ecosystem felt more familiar. Expo specifically was the right call for a solo founder — the managed workflow gets you from idea to TestFlight equivalent without spending a week configuring native build chains. The EAS Build system in particular is genuinely excellent.&lt;/p&gt;
&lt;p&gt;PostgreSQL was a non-negotiable. I've seen enough &quot;just use Firestore&quot; projects hit a wall the moment they needed anything resembling a JOIN, and for a health app with users, fasts, weights, water intake, community posts, and notification jobs all needing to relate to each other — a relational database was the only sane choice.&lt;/p&gt;
&lt;p&gt;RTK Query for data fetching was one of the better decisions I made early. The automatic caching, invalidation, and optimistic updates it provides out of the box took a huge amount of complexity off the table on the client side. Pairing it with AsyncStorage for persistence meant the app works properly offline without any heroic effort.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-hardest-part-modelling-time&quot;&gt;The Hardest Part: Modelling Time&lt;/h3&gt;
&lt;p&gt;Fasting is fundamentally about time. But time in software is famously treacherous — especially when your users are in 40+ different timezones.&lt;/p&gt;
&lt;p&gt;The core data model for a fast looks simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE fasts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id),
  status VARCHAR(20) NOT NULL DEFAULT 'SCHEDULED',
  timezone VARCHAR(100) NOT NULL,
  start_at TIMESTAMPTZ NOT NULL,
  end_at TIMESTAMPTZ NOT NULL,
  actual_start_at TIMESTAMPTZ,
  actual_end_at TIMESTAMPTZ,
  duration_target_mins INT NOT NULL,
  duration_actual_mins INT,
  ...
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trap I nearly fell into: storing times as local timestamps without timezone. I'd seen this mistake in other codebases and nearly made it myself anyway. &lt;code&gt;TIMESTAMPTZ&lt;/code&gt; stores everything as UTC internally and lets PostgreSQL handle timezone conversion correctly. Combined with storing the user's timezone string (&lt;code&gt;&quot;Africa/Nairobi&quot;&lt;/code&gt;, &lt;code&gt;&quot;America/New_York&quot;&lt;/code&gt;, etc.) — this meant every query, every calculation, every scheduled notification fired at the right local time.&lt;/p&gt;
&lt;p&gt;The server runs a cron job every 60 seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;setInterval(async () =&amp;gt; {
  if (cronRunning) return;
  cronRunning = true;
  try {
    await autoStartDueFasts();
    await processDue();
  } catch (e: any) {
    console.error(&quot;[cron] tick error:&quot;, e?.message ?? e);
  } finally {
    cronRunning = false;
  }
}, 60_000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;autoStartDueFasts()&lt;/code&gt; transitions &lt;code&gt;SCHEDULED&lt;/code&gt; fasts to &lt;code&gt;ACTIVE&lt;/code&gt; when their &lt;code&gt;start_at&lt;/code&gt; passes. &lt;code&gt;processDue()&lt;/code&gt; fires push notifications from a job queue. The guard (&lt;code&gt;if (cronRunning) return&lt;/code&gt;) prevents overlap on slow ticks — simple, and it works.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-feature-that-changed-everything-metabolic-stages&quot;&gt;The Feature That Changed Everything: Metabolic Stages&lt;/h3&gt;
&lt;p&gt;This is where LifeFast became a different kind of fasting app.&lt;/p&gt;
&lt;p&gt;Most fasting timers show you a percentage. 43%. 68%. 100%. These numbers don't mean anything experientially. What actually happens to your body during a fast follows a predictable, fascinating sequence — and if you show users that instead of just a countdown, everything changes.&lt;/p&gt;
&lt;p&gt;I defined fasting stages based on published metabolic research:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Stage &lt;/th&gt;&lt;th&gt;Hours &lt;/th&gt;&lt;th&gt;What's Happening&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Fed State &lt;/td&gt;&lt;td&gt;0–3h &lt;/td&gt;&lt;td&gt;Digesting, insulin elevated&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Early Fast &lt;/td&gt;&lt;td&gt;3–8h &lt;/td&gt;&lt;td&gt;Glycogen depleting, fat burning begins&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Fasting State &lt;/td&gt;&lt;td&gt;8–12h &lt;/td&gt;&lt;td&gt;Insulin low, fat oxidation accelerating&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Fat Burning &lt;/td&gt;&lt;td&gt;12–18h &lt;/td&gt;&lt;td&gt;Liver glycogen depleted, ketone production starting&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Ketosis &lt;/td&gt;&lt;td&gt;18–24h &lt;/td&gt;&lt;td&gt;Ketones measurable, metabolic switch active&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Autophagy &lt;/td&gt;&lt;td&gt;24h+ &lt;/td&gt;&lt;td&gt;Cellular self-cleaning in full swing&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each stage has a name, a description, a colour, and a start/end threshold. On the client, I compute which stage the user is in at render time based on elapsed hours:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function getFastingStage(elapsedHours: number): FastingStage {
  const stages = [
    { label: &quot;Fed State&quot;, color: &quot;#94a3b8&quot;, minHours: 0, maxHours: 3 },
    { label: &quot;Early Fast&quot;, color: &quot;#38bdf8&quot;, minHours: 3, maxHours: 8 },
    { label: &quot;Fat Burning&quot;, color: &quot;#fb923c&quot;, minHours: 8, maxHours: 18 },
    { label: &quot;Ketosis&quot;, color: &quot;#a78bfa&quot;, minHours: 18, maxHours: 24 },
    { label: &quot;Autophagy&quot;, color: &quot;#34d399&quot;, minHours: 24, maxHours: Infinity },
  ];
  return stages.findLast((s) =&amp;gt; elapsedHours &amp;gt;= s.minHours) ?? stages[0];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the app's circular timer, the active stage label shows in its colour — a glowing cyan or violet or green dot next to the stage name. Users started messaging me: &lt;em&gt;&quot;I just hit Ketosis for the first time!&quot;&lt;/em&gt; — not because I told them to care, but because seeing it named and coloured in real time made it real.&lt;/p&gt;
&lt;p&gt;That's the difference between showing someone a number and showing them a story about their own biology.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;building-the-community-layer&quot;&gt;Building the Community Layer&lt;/h3&gt;
&lt;p&gt;About two months in, I realised I was building a productivity tool when I should have been building a social one. Fasting is hard in isolation. It's dramatically easier when you're doing it alongside people who understand the challenge.&lt;/p&gt;
&lt;p&gt;So I built a full community layer: groups, posts, comments, nested replies, likes, and a moderation system.&lt;/p&gt;
&lt;p&gt;The schema is relatively conventional, but a few decisions were non-obvious.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Soft deletes for moderation.&lt;/strong&gt; When a user reports a post and an admin hides it, the content isn't deleted — it's flagged:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE community_posts
  ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT FALSE,
  ADD COLUMN hidden_reason TEXT,
  ADD COLUMN hidden_by UUID REFERENCES users(id),
  ADD COLUMN hidden_at TIMESTAMPTZ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means we can review decisions, reverse them, and maintain an audit trail. Hard deletes felt wrong for a moderation workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pinned posts per group.&lt;/strong&gt; The query for listing posts handles pinning cleanly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ORDER BY p.is_pinned DESC, p.created_at DESC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two-token sort. Pin always wins; recency breaks ties. Simple and fast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The notification fan-out problem.&lt;/strong&gt; When a post in a popular group gets a comment, who gets notified? The author only. When a comment gets a reply, the parent comment's author gets notified. I built a lightweight &lt;code&gt;notification_jobs&lt;/code&gt; table and a worker that processes it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE notification_jobs (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL REFERENCES users(id),
  kind VARCHAR(50) NOT NULL,
  scheduled_for TIMESTAMPTZ NOT NULL,
  payload JSONB,
  status VARCHAR(20) DEFAULT 'pending',
  attempts INT DEFAULT 0
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The worker polls every 60 seconds, picks up pending jobs past their &lt;code&gt;scheduled_for&lt;/code&gt; time, calls Expo's push notification API, and marks them as sent or failed. Dead simple, fully auditable, no external queue infrastructure needed at this scale.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-billing-nightmare&quot;&gt;The Billing Nightmare&lt;/h3&gt;
&lt;p&gt;I'm going to be honest: Google Play Billing is the worst API I have ever worked with professionally.&lt;/p&gt;
&lt;p&gt;The documentation is spread across three different Google sites that contradict each other. The test environment behaves differently from production in ways that are not documented. The error codes are numerical and the mapping table is buried six pages deep in a section titled &quot;Deprecated APIs&quot;.&lt;/p&gt;
&lt;p&gt;Here's what eventually worked:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use the &lt;code&gt;google-play-billing&lt;/code&gt; library on the React Native side for purchase flow&lt;/li&gt;
&lt;li&gt;On the backend, verify every purchase token against the Google Play Developer API before granting entitlements&lt;/li&gt;
&lt;li&gt;Store the raw &lt;code&gt;purchase_token&lt;/code&gt; and &lt;code&gt;product_id&lt;/code&gt; — you'll need both for future verification and refund handling&lt;/li&gt;
&lt;li&gt;Handle &lt;code&gt;ITEM_ALREADY_OWNED&lt;/code&gt; (error code 7) gracefully — it means the user purchased on another account or device and is more common than you'd think&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The verification endpoint on my backend:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;router.post(&quot;/billing/google/verify&quot;, authenticate, async (req, res) =&amp;gt; {
  const { purchaseToken, productId, packageName } = req.body;

  const verified = await verifyGooglePurchase(packageName, productId, purchaseToken);
  if (!verified) return res.status(400).json({ success: false, error: &quot;Invalid purchase&quot; });

  const entitlement = getEntitlementForProduct(productId);
  await billingRepo.recordPurchase({ userId: req.user.id, purchaseToken, productId, entitlement });
  await usersRepo.grantEntitlement(req.user.id, entitlement);

  res.json({ success: true, entitlement });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lesson: never grant entitlements on the client side. Always verify on the server. This is not optional.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;shipping-eas-build-and-deployment&quot;&gt;Shipping: EAS Build and Deployment&lt;/h3&gt;
&lt;p&gt;The CI/CD pipeline runs on GitHub Actions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Push to &lt;code&gt;main&lt;/code&gt; triggers the workflow&lt;/li&gt;
&lt;li&gt;TypeScript compiles, then the Express app is bundled&lt;/li&gt;
&lt;li&gt;SSH into the production DigitalOcean Droplet&lt;/li&gt;
&lt;li&gt;Pull latest, rebuild Docker container, swap with zero-downtime restart&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the mobile app, EAS Build handles the actual Android build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;eas build --platform android --profile production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then submit to Google Play via:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;eas submit --platform android
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EAS Submit directly uploads the AAB to the Play Store internal track. For a solo founder, this is a genuinely transformative improvement over the old &quot;manually drag an AAB file into the Play Console&quot; workflow.&lt;/p&gt;
&lt;p&gt;One lesson I'd pass on: set up your production and staging environments early. I shipped several breaking changes to all users before I separated the two, and the user experience was awful. A staging environment you actually test on is not optional — it's the difference between professionalism and chaos.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-got-wrong&quot;&gt;What I Got Wrong&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Email broadcast timing.&lt;/strong&gt; I sent an announcement to my users about a new release. I wrote the batch-sending code to fire 50 emails in parallel. I had about 250 users at the time. The email API rate limits at 5 requests per second. You can do the math. Half my users got error responses instead of emails, and the 300ms sequential delay I needed was sitting there in the documentation the whole time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Over-engineering notifications early.&lt;/strong&gt; I spent a week building a sophisticated notification scheduling system before I had 50 users. The simpler version — a cron job and a jobs table — does the same job. I could have built it in a day and shipped the features users actually needed instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not adding the progress percentage cap sooner.&lt;/strong&gt; The fasting percentage can technically exceed 100% if someone continues fasting past their target window. For a while, my timer showed &quot;127%&quot; in a progress circle — which is both mathematically accurate and completely absurd. A one-line fix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const percentage = Math.min(100, Math.max(0, Math.round(progressRatio * 100)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Users notice things like this. They form impressions of quality from small details. Fix the small things before they become the big things.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-product-is-alive&quot;&gt;The Product Is Alive&lt;/h3&gt;
&lt;p&gt;LifeFast is available now on Android.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://play.google.com/store/apps/details?id=com.jane.ndirangu.lifefast&quot;&gt;Download on Google Play&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://lifefast.online&quot;&gt;Website: lifefast.online&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's free. It tracks your fasting window in real time, shows you your metabolic stage as you progress, logs your water intake, tracks your weight over time, lets you log progress photos, and connects you with a community of other fasters doing the same work.&lt;/p&gt;
&lt;p&gt;I built this without a team, without investors, and without a roadmap handed to me by a product manager. Just a problem I cared about, a stack I understood, and enough stubbornness to keep shipping.&lt;/p&gt;
&lt;p&gt;If you're building something similar — a health app, a behaviour-change product, anything where timing and user psychology overlap — feel free to reach out. I'd genuinely enjoy the conversation.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19608/building-lifefast-a-solo-founders-deep-dive-into-fasting-app-architecture</guid>
<pubDate>Wed, 03 Jun 2026 19:31:21 +0000</pubDate>
</item>
<item>
<title>Technical Post-Mortem: Debugging AdMob Impression Leaks in an Expo/Firebase Mobile App</title>
<link>https://coderlegion.com/19057/technical-post-mortem-debugging-admob-impression-leaks-in-an-expo-firebase-mobile-app</link>
<description>&lt;p&gt;As a mobile developer, shipping an app to production is only half the battle. The real complications begin when analyzing live telemetry and monetization data.&lt;/p&gt;
&lt;p&gt;I recently launched a personal intermittent fasting tracker built on React Native (Expo) and Firebase called LifeFast. The app uses an ad-supported model to offset background hosting and real-time community feed sync costs.&lt;/p&gt;
&lt;p&gt;After reviewing my first week of live AdMob metrics, I noticed a major bottleneck in the rendering lifecycle: a severe disconnect between Ad Requests and Actual Impressions.&lt;/p&gt;
&lt;p&gt;The Data Log&lt;br&gt;
Total Requests: 1.11K (Strong API connectivity)&lt;/p&gt;
&lt;p&gt;Match Rate: 70.03% (AdMob is successfully returning inventory)&lt;/p&gt;
&lt;p&gt;Impressions: 345 (The bottleneck)&lt;/p&gt;
&lt;p&gt;Out of ~770 successfully returned ad units, less than half are registering as viewed by users.&lt;/p&gt;
&lt;p&gt;Breakdown by Ad Unit Lifecycle&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Interstitials (eCPM: €1.50 | 44 Impressions)&lt;br&gt;
The eCPM here is the highest, but the volume is low.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Root Cause: The layout logic was attempting to load and display the interstitial simultaneously during screen state transitions. If the user navigates faster than the network request completes, the ad is dropped, resulting in a matched request but zero impressions.&lt;/p&gt;
&lt;p&gt;The Fix: Migrating to a pre-loading architecture using ad.load() immediately upon component mount, caching the unit globally, and only executing ad.show() during natural UX breakpoints.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;App Open Ads (eCPM: €1.46 | 139 Impressions)&lt;br&gt;
The Root Cause: The current trigger relies heavily on a cold app start. If a user backgrounds the app to check a message and resumes their fast tracking, the lifecycle event isn't consistently capturing the background-to-foreground transition.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Fix: Listening directly to React Native's AppState changes to ensure the ad logic evaluates state changes from background -&amp;gt; active.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Banner Ads (eCPM: €0.21 | 162 Impressions)&lt;br&gt;
The Root Cause: Standard fixed-size banners are occasionally clipping outside the viewport on varying Android aspect ratios, firing the request script but failing the viewability requirements.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Fix: Deprecating fixed widths in favor of Adaptive Banners that dynamically calculate the screen width container before requesting the asset.&lt;/p&gt;
&lt;p&gt;Marketing an Independent Project on a $0 Budget&lt;br&gt;
The other engineering challenge is organic discovery. Traditional social platforms have highly aggressive automated spam filters that auto-flag direct Play Store links, making standard community outreach unviable.&lt;/p&gt;
&lt;p&gt;To bypass this without a marketing budget, I am currently implementing a code-driven viral loop using react-native-share to let users natively export progress milestones to their personal networks, shifting the acquisition logic directly into the application codebase.&lt;/p&gt;
&lt;p&gt;For context on the layout implementation or to see how the live UI handles these ad placements, the application is live on the Google Play Store under &lt;a rel=&quot;nofollow&quot; href=&quot;https://play.google.com/store/apps/details?id=com.jane.ndirangu.lifefast&quot;&gt;LifeFast.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If anyone has optimized the match-to-impression ratio inside an Expo ecosystem, I'd be interested in comparing lifecycle architectures in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19057/technical-post-mortem-debugging-admob-impression-leaks-in-an-expo-firebase-mobile-app</guid>
<pubDate>Wed, 27 May 2026 11:13:12 +0000</pubDate>
</item>
<item>
<title>Top 4 React Native Image Caching Libraries in 2026</title>
<link>https://coderlegion.com/18299/top-4-react-native-image-caching-libraries-in2026</link>
<description>&lt;p&gt;An opinionated guide to image caching in React Native, from battle-tested classics to the blazing-fast newcomer built for the New Architecture.&lt;/p&gt;
&lt;p&gt;Caching images in React Native has always been painful. The default &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component is slow, flickers on reloads, and offers almost zero control.&lt;br&gt;
For years developers have relied on a handful of libraries but most of them were built before the New Architecture, before Turbo Modules or Nitro Modules, and before apps started demanding smooth video caching and zero-copy performance.&lt;/p&gt;
&lt;p&gt;In 2026, the game has changed. Here are the Top 4 image caching libraries I recommend today, ranked by real-world usage, performance, and developer experience.&lt;br&gt;
Make sure you check out the last one!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1&lt;/strong&gt; &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-native-fast-image&quot;&gt;react-native-fast-image&lt;/a&gt;&lt;/strong&gt; - The Veteran King (Still Dominant)&lt;/p&gt;
&lt;p&gt;The library is no longer actively maintained but still being used by many. I strongly recommend the fork - &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@d11/react-native-fast-image&quot;&gt;@d11/react-native-fast-image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Pros&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Battle-tested for years&lt;/li&gt;
&lt;li&gt;Excellent image prioritization and cache control&lt;/li&gt;
&lt;li&gt;Huge community&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cons&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;New Architecture support is patchy (community fork required)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Higher bridge overhead compared to modern Nitro solutions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Still the go-to for many production apps, but it's starting to show its age.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2&lt;/strong&gt; &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/expo-image&quot;&gt;expo-image&lt;/a&gt;&lt;/strong&gt; - The Expo Ecosystem Favorite&lt;/p&gt;
&lt;p&gt;Pros&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Beautiful API and great DX&lt;/li&gt;
&lt;li&gt;Excellent New Architecture support&lt;/li&gt;
&lt;li&gt;Built-in memory + disk caching with cachePolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cons&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tied to the Expo ecosystem (less ideal for bare React Native)&lt;/li&gt;
&lt;li&gt;Limited programmatic control (no easy remove(url) or cache inspection)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Perfect if you're already in Expo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-native-turbo-image&quot;&gt;&lt;strong&gt;react-native-turbo-image&lt;/strong&gt;&lt;/a&gt; - The Modern Native Contender&lt;/p&gt;
&lt;p&gt;Built on Nuke (iOS) and Coil (Android), this library delivers excellent native performance and is gaining traction fast.&lt;/p&gt;
&lt;p&gt;Pros&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Very fast native image loading&lt;/li&gt;
&lt;li&gt;Good New Architecture compatibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cons&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Smaller community than fast-image&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A strong option if you want native speed without Nitro.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-native-nitro-cache&quot;&gt;&lt;strong&gt;react-native-nitro-cache&lt;/strong&gt;&lt;/a&gt; - The Newcomer That's Stealing the Show&lt;/p&gt;
&lt;p&gt;This is the one I'm most excited about in 2026.&lt;br&gt;
Built from the ground up with Nitro Modules + C++, react-native-nitro-cache is a unified asset cache for images, videos, and any binary files. Has a very simple api interface, very easy to use.&lt;/p&gt;
&lt;p&gt;Why it stands out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unified caching (images + videos + arbitrary files)&lt;/li&gt;
&lt;li&gt;Zero-copy ArrayBuffer support for raw binary data&lt;/li&gt;
&lt;li&gt;Blazing-fast in-memory retrieval with safe atomic persistence&lt;/li&gt;
&lt;li&gt;Built-in TTL, programmatic remove(url) and cache inspection&lt;/li&gt;
&lt;li&gt;Full New Architecture support out of the box&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It feels like someone finally built the cache library we've all been waiting for since the New Architecture landed.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;react-native-fast-image&lt;/strong&gt; and &lt;strong&gt;expo-image&lt;/strong&gt; are still solid choices, they are battle tested and the community love them.&lt;/p&gt;
&lt;p&gt;My 2026 recommendation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expo-heavy project → stick with expo-image&lt;/li&gt;
&lt;li&gt;Need maximum battle-tested stability → react-native-fast-image (with the community fork)&lt;/li&gt;
&lt;li&gt;Want the future of unified, high-performance caching → try &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-native-nitro-cache&quot;&gt;react-native-nitro-cache&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The library is brand new, actively developed, and already solving real pain points that the older solutions never addressed.&lt;br&gt;
Have you tried it yet? Drop a comment below with your current caching setup. I'd love to hear what's working (or not working) for you in 2026.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18299/top-4-react-native-image-caching-libraries-in2026</guid>
<pubDate>Tue, 19 May 2026 08:39:54 +0000</pubDate>
</item>
<item>
<title>Parsing hundreds of megabytes of JSON in milliseconds in React Native</title>
<link>https://coderlegion.com/18298/parsing-hundreds-of-megabytes-of-json-in-milliseconds-in-react-native</link>
<description>&lt;p&gt;&lt;em&gt;How &lt;code&gt;JSON.parse&lt;/code&gt; materializes entire documents in JavaScript, and how &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ifeoluwak/react-native-fast-json&quot;&gt;react-native-fast-json&lt;/a&gt; uses native simdjson + lazy &lt;code&gt;JsonView&lt;/code&gt; access to change the cost model in React Native.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ifeoluwak/react-native-fast-json&quot;&gt;react-native-fast-json&lt;/a&gt;&lt;/strong&gt; is an open-source React Native library that wraps &lt;strong&gt;simdjson&lt;/strong&gt; in C++ and exposes a lazy &lt;strong&gt;&lt;code&gt;JsonView&lt;/code&gt;&lt;/strong&gt; through &lt;a rel=&quot;nofollow&quot; href=&quot;https://nitro.margelo.com/&quot;&gt;Nitro Modules&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Most teams treat JSON parsing as “cheap” because API responses are small. The mental model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;bytes on wire → JSON.parse → plain objects in JS → app logic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That model breaks when the payload is &lt;strong&gt;hundreds of megabytes&lt;/strong&gt;. The dominant cost is not “JSON syntax is slow” in the abstract, it is &lt;strong&gt;allocating and wiring up a full value tree in the JavaScript heap&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-json-parse-actually-does-in-react-native&quot;&gt;What &lt;code&gt;JSON.parse&lt;/code&gt; actually does in React Native&lt;/h3&gt;
&lt;p&gt;When you call &lt;code&gt;JSON.parse&lt;/code&gt; (or &lt;code&gt;await response.json()&lt;/code&gt;), the engine:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Holds the &lt;strong&gt;UTF-16/UTF-8 string&lt;/strong&gt; (or an internal representation of the body)&lt;/li&gt;
&lt;li&gt;Walks the entire document&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;every&lt;/strong&gt; object, array, number, boolean, and string, creates &lt;strong&gt;JavaScript values&lt;/strong&gt; with engine overhead (hidden classes, property storage, array backing stores, interned strings, etc.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a &lt;strong&gt;~250 MB&lt;/strong&gt; file, outcomes depend heavily on &lt;strong&gt;platform and available RAM&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Symptom &lt;/th&gt;&lt;th&gt;Typical cause&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Multi-second freezes&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Single-threaded parse + massive allocation on the JS thread&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Memory far above file size&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;JS object graph overhead (often &lt;strong&gt;~2–4×&lt;/strong&gt; the raw JSON size in bad cases)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;OOM / process kill&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Peak JS heap + retained tree + rest of the app&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;On the &lt;strong&gt;&lt;code&gt;data_250mb.json&lt;/code&gt;&lt;/strong&gt; fixture in our tests: &lt;strong&gt;iOS&lt;/strong&gt; could complete &lt;code&gt;JSON.parse&lt;/code&gt; but only with &lt;strong&gt;extreme CPU and memory spikes&lt;/strong&gt;; &lt;strong&gt;Android&lt;/strong&gt; &lt;strong&gt;always crashed&lt;/strong&gt; before parse finished, making &lt;code&gt;JSON.parse&lt;/code&gt; &lt;strong&gt;impossible&lt;/strong&gt; for that payload on Android, while native &lt;code&gt;parseFile&lt;/code&gt; worked on both platforms.&lt;/p&gt;
&lt;h4 id=&quot;benchmark-fixture-source-&quot;&gt;Benchmark fixture (source)&lt;/h4&gt;
&lt;p&gt;Measurements in this repo use the public &lt;strong&gt;~250 MB&lt;/strong&gt; JSON from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/antonmedv/json-examples&quot;&gt;antonmedv/json-examples&lt;/a&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Repository&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/antonmedv/json-examples&quot;&gt;https://github.com/antonmedv/json-examples&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;File&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/antonmedv/json-examples/blob/master/data_250mb.json&quot;&gt;&lt;code&gt;data_250mb.json&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Raw URL&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;https://github.com/antonmedv/json-examples/raw/refs/heads/master/data_250mb.json&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ifeoluwak/react-native-fast-json/tree/main/example&quot;&gt;example app&lt;/a&gt; downloads that URL, caches it on disk, then calls &lt;code&gt;parseFile&lt;/code&gt; on the local path.&lt;/p&gt;
&lt;h4 id=&quot;measured-results-parsefile-release-builds-physical-devices-&quot;&gt;Measured results (&lt;code&gt;parseFile&lt;/code&gt;, release builds, physical devices)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Platform &lt;/th&gt;&lt;th&gt;Time to root &lt;code&gt;JsonView&lt;/code&gt; &lt;/th&gt;&lt;th&gt;CPU / memory (observed)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;iOS&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~100 ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Stable&lt;/strong&gt;, no long CPU peg, no runaway JS heap growth during parse&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Android&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~500 ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Stable&lt;/strong&gt;, same qualitative behavior as iOS, slower wall time&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;JSON.parse&lt;/code&gt; / &lt;code&gt;response.json()&lt;/code&gt;&lt;/strong&gt; on the same fixture:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Platform &lt;/th&gt;&lt;th&gt;&lt;code&gt;JSON.parse&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;parseFile&lt;/code&gt; + lazy &lt;code&gt;JsonView&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;iOS&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Many seconds&lt;/strong&gt;; &lt;strong&gt;extreme&lt;/strong&gt; CPU and memory spikes (~&lt;strong&gt;1.2 GB&lt;/strong&gt; JS heap ballpark) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~100 ms&lt;/strong&gt;; CPU and memory &lt;strong&gt;stable&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Android&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Crash&lt;/strong&gt;, could &lt;strong&gt;not&lt;/strong&gt; complete parse &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~500 ms&lt;/strong&gt;; CPU and memory &lt;strong&gt;stable&lt;/strong&gt; (~&lt;strong&gt;400 MB&lt;/strong&gt; mostly &lt;strong&gt;native&lt;/strong&gt; buffer)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The second row is &lt;strong&gt;not&lt;/strong&gt; “free memory.” The file still occupies roughly &lt;strong&gt;file size + simdjson padding&lt;/strong&gt; in native memory. The win is &lt;strong&gt;avoiding a second full copy as a giant JS object graph&lt;/strong&gt; until you explicitly materialize pieces.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-read-file-as-string-then-json-parse-can-be-worse-at-peak&quot;&gt;Why “read file as string, then &lt;code&gt;JSON.parse&lt;/code&gt;” can be worse at peak&lt;/h3&gt;
&lt;p&gt;A common variant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const text = await readFile(path, 'utf8');
const data = JSON.parse(text);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At peak you may briefly hold:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;entire file as one JS string&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;entire parsed tree&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Parser scratch allocations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;response.json()&lt;/code&gt; can sometimes avoid exposing one giant application-level string, but the &lt;strong&gt;end state&lt;/strong&gt; is still the same: a full tree in JS. For large files, &lt;strong&gt;peak heap&lt;/strong&gt; is the killer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-technique-how-react-native-fast-json-does-it&quot;&gt;The technique: how react-native-fast-json does it&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ifeoluwak/react-native-fast-json&quot;&gt;react-native-fast-json&lt;/a&gt; is what makes the native lazy model practical in React Native. Instead of returning a plain object from &lt;code&gt;JSON.parse&lt;/code&gt;, it shifts work across a different boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;file bytes → native buffer (simdjson) → JsonView handle → read paths on demand
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;1-native-buffer-simdjson-&quot;&gt;1. Native buffer (simdjson)&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;parseFile&lt;/code&gt; loads the file into a &lt;strong&gt;padded&lt;/strong&gt; native buffer (&lt;code&gt;simdjson::padded_string::load&lt;/code&gt;). Parsing runs in &lt;strong&gt;C++&lt;/strong&gt; with &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/simdjson/simdjson&quot;&gt;simdjson&lt;/a&gt; (SIMD-accelerated, widely used for large JSON).&lt;/p&gt;
&lt;p&gt;The document’s canonical bytes live in &lt;strong&gt;native memory&lt;/strong&gt;, not as nested JS objects.&lt;/p&gt;
&lt;h4 id=&quot;2-lazy-jsonview-nitro-hybrid-object-&quot;&gt;2. Lazy &lt;code&gt;JsonView&lt;/code&gt; (Nitro hybrid object)&lt;/h4&gt;
&lt;p&gt;The root returned to JavaScript is a &lt;strong&gt;&lt;code&gt;JsonView&lt;/code&gt;&lt;/strong&gt;, a &lt;a rel=&quot;nofollow&quot; href=&quot;https://nitro.margelo.com/&quot;&gt;Nitro&lt;/a&gt; hybrid object backed by C++. Calling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getValue('metadata')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;atPath('$.a.b.c')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asString()&lt;/code&gt; on a scalar&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…does &lt;strong&gt;targeted&lt;/strong&gt; native work and only crosses the bridge with &lt;strong&gt;small&lt;/strong&gt; results (another &lt;code&gt;JsonView&lt;/code&gt; handle or a primitive), not the whole document.&lt;/p&gt;
&lt;h4 id=&quot;3-materialization-is-explicit-and-expensive&quot;&gt;3. Materialization is explicit and expensive&lt;/h4&gt;
&lt;p&gt;When you call:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;asObject()&lt;/code&gt;&lt;/strong&gt;, subtree becomes a JS-friendly map/array structure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rawJson()&lt;/code&gt;&lt;/strong&gt;, subtree becomes a JS string&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…you have chosen to &lt;strong&gt;pay the JS heap cost&lt;/strong&gt; for that slice. On a 250 MB document, calling these on the &lt;strong&gt;root&lt;/strong&gt; is equivalent to opting back into the problem you avoided.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture&quot;&gt;Architecture&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;HybridFastJson&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;cpp/HybridFastJson.cpp&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;parseFile(path)&lt;/code&gt;, load file, construct &lt;code&gt;HybridJsonView&lt;/code&gt;, &lt;strong&gt;cache&lt;/strong&gt; by path string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parseString(str)&lt;/code&gt;, copy string into native view (&lt;strong&gt;not&lt;/strong&gt; path-cached)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;release(path)&lt;/code&gt;, erase cache entry for that path&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;HybridJsonView&lt;/code&gt;&lt;/strong&gt;, navigation, path helpers, scalar coercion, optional materialization.&lt;/p&gt;
&lt;p&gt;Until &lt;code&gt;release(path)&lt;/code&gt;, a cached &lt;code&gt;parseFile&lt;/code&gt; keeps the &lt;strong&gt;full native buffer&lt;/strong&gt; alive even if JS drops its &lt;code&gt;JsonView&lt;/code&gt; reference, plan releases when the flow ends.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-you-still-pay-for-honest-accounting-&quot;&gt;What you still pay for (honest accounting)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Cost &lt;/th&gt;&lt;th&gt;Still there?&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;File-sized native memory&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Yes, roughly &lt;strong&gt;JSON size + padding&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Parse CPU&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Yes, but native simdjson is much faster than building a JS tree&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;JS heap for the whole tree&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;, unless you &lt;code&gt;asObject()&lt;/code&gt; / &lt;code&gt;rawJson()&lt;/code&gt; large parts&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bridge traffic&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Per access; keep reads coarse-grained for huge docs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Path cache&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;parseFile&lt;/code&gt; retains buffer until &lt;strong&gt;&lt;code&gt;release(path)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;There is &lt;strong&gt;no zero memory overhead&lt;/strong&gt;. The claim that holds up is: &lt;strong&gt;near-zero &lt;em&gt;additional&lt;/em&gt; JS heap for the full document&lt;/strong&gt; while you navigate lazily.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-milliseconds-or-low-hundreds-of-ms-are-plausible-for-hundreds-of-mb&quot;&gt;Why milliseconds (or low hundreds of ms) are plausible for hundreds of MB&lt;/h3&gt;
&lt;p&gt;simdjson is designed for throughput on large valid JSON. On the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/antonmedv/json-examples/blob/master/data_250mb.json&quot;&gt;&lt;code&gt;data_250mb.json&lt;/code&gt;&lt;/a&gt; fixture, &lt;strong&gt;&lt;code&gt;parseFile&lt;/code&gt; after the file is on disk&lt;/strong&gt; landed at &lt;strong&gt;~100 ms on iOS&lt;/strong&gt; and &lt;strong&gt;~500 ms on Android&lt;/strong&gt; in our release builds, with &lt;strong&gt;stable CPU and memory&lt;/strong&gt;. By contrast, &lt;strong&gt;&lt;code&gt;JSON.parse&lt;/code&gt; on iOS&lt;/strong&gt; took &lt;strong&gt;many seconds&lt;/strong&gt; with &lt;strong&gt;extreme spikes&lt;/strong&gt;; on &lt;strong&gt;Android&lt;/strong&gt; it &lt;strong&gt;always crashed&lt;/strong&gt;, so native parse was the only viable option there.&lt;/p&gt;
&lt;p&gt;Caveats when you reproduce benchmarks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fixture&lt;/strong&gt;, use the same public file or disclose yours&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cold vs warm&lt;/strong&gt; storage (first read after download vs re-parse)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform&lt;/strong&gt;, Android was ~5× slower than iOS in our tests; do not assume one number for all devices&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device tier&lt;/strong&gt; and storage speed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON shape&lt;/strong&gt; (depth, string density) affects constant factors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debug vs release&lt;/strong&gt; native builds, ship measurements on &lt;strong&gt;release&lt;/strong&gt; binaries&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;comparison-table-same-fixture-measured-observed-&quot;&gt;Comparison table (same fixture, measured + observed)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Dimension &lt;/th&gt;&lt;th&gt;&lt;code&gt;JSON.parse&lt;/code&gt; (our tests) &lt;/th&gt;&lt;th&gt;Native &lt;code&gt;JsonView&lt;/code&gt; (&lt;code&gt;parseFile&lt;/code&gt;)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;iOS&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Completes after &lt;strong&gt;many seconds&lt;/strong&gt;; &lt;strong&gt;extreme&lt;/strong&gt; CPU/memory spikes &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~100 ms&lt;/strong&gt;; &lt;strong&gt;stable&lt;/strong&gt; CPU/memory&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Android&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Crash&lt;/strong&gt;, parse &lt;strong&gt;not possible&lt;/strong&gt; on ~250 MB &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~500 ms&lt;/strong&gt;; &lt;strong&gt;stable&lt;/strong&gt; CPU/memory&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Primary memory&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Full JS object graph (when it survives) &lt;/td&gt;&lt;td&gt;Native padded buffer (~file size)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;UI thread risk&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;High during parse &lt;/td&gt;&lt;td&gt;Lower; still avoid huge sync work in JS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Best access pattern&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Whole-tree mutation &lt;/td&gt;&lt;td&gt;Few paths / scalars / wildcards&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Foot-guns&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Retaining whole &lt;code&gt;data&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;asObject()&lt;/code&gt; / &lt;code&gt;rawJson()&lt;/code&gt; on huge nodes; never calling &lt;code&gt;release&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wildcards-and-paths&quot;&gt;Wildcards and paths&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;atPath('$.a.b.c')&lt;/code&gt;&lt;/strong&gt;, simple dotted segments, no &lt;code&gt;[index]&lt;/code&gt; in the path string.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;`atPathWithWildcard('$.items[&lt;/em&gt;].id')&lt;code&gt;**, dynamic segments; returns &lt;/code&gt;string[] | null` for matched scalar strings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For very large documents, prefer &lt;strong&gt;narrow&lt;/strong&gt; paths over scanning wildcards across the whole tree unless you have measured the cost.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-native-lazy-parsing-is-the-wrong-tool&quot;&gt;When native lazy parsing is the wrong tool&lt;/h3&gt;
&lt;h4 id=&quot;not-a-common-use-case-prefer-better-alternatives&quot;&gt;Not a common use case, prefer better alternatives&lt;/h4&gt;
&lt;p&gt;Shipping &lt;strong&gt;~250 MB JSON&lt;/strong&gt; to a phone is a &lt;strong&gt;smell&lt;/strong&gt;, not a pattern. We benchmark it because it stress-tests the parser and matches real legacy pain, not because every app should do it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try these first&lt;/strong&gt; (usually cheaper than optimizing parse):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Don’t send the file&lt;/strong&gt;, paginate, stream, or return only fields the UI needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t use JSON on the wire&lt;/strong&gt;, schema-driven binary formats decode faster and smaller.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t keep one blob&lt;/strong&gt;, SQLite (or similar) with indexes for the queries you actually run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t parse on the critical path&lt;/strong&gt;, generate summaries or shards on the server; sync deltas.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Native lazy parsing is a &lt;strong&gt;mitigation&lt;/strong&gt; when you are locked into large JSON. It is not a substitute for a sane data contract.&lt;/p&gt;
&lt;h4 id=&quot;when-this-library-still-does-not-help-enough&quot;&gt;When this library still does not help enough&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Small payloads, &lt;code&gt;JSON.parse&lt;/code&gt; is simpler and fast enough.&lt;/li&gt;
&lt;li&gt;You need the entire document in JS anyway, you will materialize everything eventually; measure whether native parse + bulk &lt;code&gt;asObject()&lt;/code&gt; is still a win.&lt;/li&gt;
&lt;li&gt;Heavy in-place mutation of arbitrary subtrees in JS, work on plain objects.&lt;/li&gt;
&lt;li&gt;You cannot hold &lt;strong&gt;file-sized native RAM&lt;/strong&gt; (low-RAM devices + multiple huge files), budget and serialize access; even native buffers are ~file size.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;benchmark-methodology&quot;&gt;Benchmark methodology&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { fastJson } from 'react-native-fast-json';

const root = await fastJson.parseFile('/path/to/large.json');
const version = root?.getValue('metadata')?.getValue('version')?.asString();
fastJson.release('/path/to/large.json');
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fixture&lt;/strong&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/antonmedv/json-examples/blob/master/data_250mb.json&quot;&gt;&lt;code&gt;data_250mb.json&lt;/code&gt;&lt;/a&gt; from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/antonmedv/json-examples&quot;&gt;antonmedv/json-examples&lt;/a&gt;, via&lt;br&gt;
&lt;code&gt;https://github.com/antonmedv/json-examples/raw/refs/heads/master/data_250mb.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RN Version&lt;/strong&gt; 0.85.0, New Architecture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device&lt;/strong&gt;, physical iOS and Android (document model, OS, RN version, Hermes on/off).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt;, release native binary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scenarios&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;A: &lt;code&gt;response.json()&lt;/code&gt; or &lt;code&gt;JSON.parse&lt;/code&gt; after full string in memory&lt;/li&gt;
&lt;li&gt;B: download/cache to disk → &lt;code&gt;parseFile(localPath)&lt;/code&gt; → read N fields → &lt;code&gt;release(path)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metrics&lt;/strong&gt;, wall time to root &lt;code&gt;JsonView&lt;/code&gt;, JS heap snapshot, CPU/memory stability in Instruments / Android Studio.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Our results&lt;/strong&gt;, &lt;code&gt;parseFile&lt;/code&gt;: iOS &lt;strong&gt;~100 ms&lt;/strong&gt;, Android &lt;strong&gt;~500 ms&lt;/strong&gt;, CPU/memory &lt;strong&gt;stable&lt;/strong&gt;. &lt;code&gt;JSON.parse&lt;/code&gt;: iOS &lt;strong&gt;many seconds&lt;/strong&gt; with &lt;strong&gt;extreme spikes&lt;/strong&gt;; Android &lt;strong&gt;always crashed&lt;/strong&gt; on this fixture.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ifeoluwak/react-native-fast-json/tree/main/example&quot;&gt;example app&lt;/a&gt; implements download + &lt;code&gt;parseFile&lt;/code&gt; + timing UI, use it as a starting point, not as universal truth.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Idea &lt;/th&gt;&lt;th&gt;Detail&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Problem&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;JSON.parse&lt;/code&gt; &lt;strong&gt;materializes&lt;/strong&gt; the whole document in the JS heap.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Technique&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Parse in &lt;strong&gt;native&lt;/strong&gt; with simdjson; expose &lt;strong&gt;&lt;code&gt;JsonView&lt;/code&gt;&lt;/strong&gt;; read &lt;strong&gt;lazily&lt;/strong&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;&lt;code&gt;data_250mb.json&lt;/code&gt;:&lt;/strong&gt; ~&lt;strong&gt;100 ms&lt;/strong&gt; (iOS) / ~&lt;strong&gt;500 ms&lt;/strong&gt; (Android) for &lt;code&gt;parseFile&lt;/code&gt; in our tests.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CPU / memory&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Stable&lt;/strong&gt; on native parse; &lt;code&gt;JSON.parse&lt;/code&gt; had &lt;strong&gt;extreme spikes&lt;/strong&gt; on iOS and &lt;strong&gt;crashed&lt;/strong&gt; on Android for this fixture.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Memory model&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Avoid blowing up the &lt;strong&gt;JS heap&lt;/strong&gt;; native still holds &lt;strong&gt;~file size&lt;/strong&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Discipline&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;release(path)&lt;/code&gt;, avoid huge &lt;code&gt;asObject()&lt;/code&gt; / &lt;code&gt;rawJson()&lt;/code&gt;, extract scalars to plain JS when done.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-react-native-fast-json&quot;&gt;Try react-native-fast-json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;yarn add react-native-fast-json react-native-nitro-modules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install pods, rebuild, then use &lt;code&gt;fastJson.parseFile&lt;/code&gt; for large files on disk. Peer dependency: &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nitro.margelo.com/&quot;&gt;react-native-nitro-modules&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18298/parsing-hundreds-of-megabytes-of-json-in-milliseconds-in-react-native</guid>
<pubDate>Tue, 19 May 2026 08:39:22 +0000</pubDate>
</item>
<item>
<title>Why Senior Architects are Banning AI from their Learning Stack</title>
<link>https://coderlegion.com/17067/why-senior-architects-are-banning-ai-from-their-learning-stack</link>
<description>&lt;p&gt;In 2026, the software industry is fiercely obsessed with one metric: velocity. We now have generative tools capable of scaffolding an entire Flutter CRUD application from a single sentence prompt.&lt;/p&gt;
&lt;p&gt;But for a Senior Architect, velocity without deep understanding isn't a superpower. It's just technical debt with a faster delivery date. My deep motivation for learning new stacks like migrating from Flutter to React Native and Expo without the aid of AI is incredibly simple: Intuition cannot be outsourced.&lt;/p&gt;
&lt;p&gt;To lead a high performing engineering team, you must understand the &quot;metal.&quot; You need to know exactly why a frame drops, how memory is allocated and garbage collected in the heap, and the precise mechanics of how the bridge communicates. If an AI writes your code, you aren't actually learning the framework; you are merely learning how to review a machine's guesswork.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &quot;Black Box&quot; of Automated Development&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The real danger of AI-driven development isn't that the code fails to work it's that it frequently works by accident. For enterprise-level applications, this creates three critical, systemic failures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architectural Drift:&lt;/strong&gt; AI fundamentally favors &quot;flat&quot; and expedient structures. It will happily embed complex business logic directly inside UI widgets because that is the shortest mathematical path to a functional output. In a scalable, long-term project, this violates Clean Architecture principles and renders comprehensive testing nearly impossible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Debugging Ceiling:&lt;/strong&gt; When you inevitably hit a high-level performance bottleneck such as noticeable lag rendering a real-time logistics map an AI-reliant developer hits a wall. Because they never manually traced the Widget Lifecycle or fully understood the Render Pipeline, they lack the foundational context required to isolate the root cause.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency Bloat:&lt;/strong&gt; LLMs frequently suggest outdated, heavy, or bloated packages to solve relatively simple problems that a senior engineer would handle natively perhaps with a custom Dart FFI or a lightweight platform channel.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Solution: My Blueprint for Manual Mastery&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To remain a top-tier engineer, I follow a &quot;Manual First&quot; approach. Here is how I ensure I actually own my knowledge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Master the Bridge, Not Just the Syntax I don't just learn how to render a list. I learn how the engine (like JavaScript/Hermes) communicates with native threads. Understanding this overhead is the difference between a &quot;working&quot; app and a &quot;smooth&quot; 120fps experience.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement Design Patterns Manually Instead of letting an LLM generate boilerplate, I manually implement patterns like BLoC. Writing the events and managing the streams myself builds a mental map of the data flow. This intuition is what allows me to predict bugs before they happen.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optimize at the Hardware Level True seniority is found where the framework ends and the hardware begins. In my work on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://pub.dev/packages/zerocopy/score&quot;&gt;zerocopy&lt;/a&gt; package, I used Dart FFI and C++ to eliminate the &quot;copy tax&quot; when handling large binary data. This level of optimization requires a deep understanding of memory that AI-generated code currently cannot replicate.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;AI is an Accelerator, Not a Teacher&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Don't get me wrong AI is incredibly important. Once you know a stack inside and out, absolutely use AI to write your repetitive boilerplate, generate your unit tests, and speed up your workflow. AI is an unmatched accelerator. But it is a terrible teacher of fundamentals.&lt;/p&gt;
&lt;p&gt;If you want to be an Architect who actually leads teams, builds scalable apps, and solves the impossible bugs, you have to build your mental map yourself. You have to write the state management manually. You have to understand how the JS engine actually talks to the native UI bridge. By choosing &quot;Manual Mastery&quot; when learning something new, you aren't being a dinosaur. You're building the foundation required to fix the complex problems that AI will inevitably create.&lt;/p&gt;
&lt;p&gt;(Want to see what this looks like in practice? Check out some of my deep-dive work at momarkhan.com).&lt;/p&gt;
&lt;p&gt;I'm curious to hear from other devs how do you balance using AI for speed vs actually learning the core of a new framework? Let's talk in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17067/why-senior-architects-are-banning-ai-from-their-learning-stack</guid>
<pubDate>Fri, 08 May 2026 13:43:06 +0000</pubDate>
</item>
<item>
<title>The Physics of Pixels: Flutter vs. React Native in 2026</title>
<link>https://coderlegion.com/15806/the-physics-of-pixels-flutter-vs-react-native-in-2026</link>
<description>&lt;p&gt;By early 2026, the &quot;Ecosystem War&quot; between Flutter and React Native (RN) has effectively reached a stalemate. If you look at GitHub stars, the volume of third-party packages, or the availability of AI-generated boilerplate, the two frameworks appear nearly identical. &lt;/p&gt;
&lt;p&gt;However, for senior engineers, the choice is no longer about &quot;which library is more popular.&quot; It’s about &lt;strong&gt;The Physics of Rendering.&lt;/strong&gt; When your application hits the performance ceiling, the only thing that matters is who owns the rendering pipeline.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-universal-rendering-model&quot;&gt;The Universal Rendering Model&lt;/h3&gt;
&lt;p&gt;Regardless of the framework, every modern UI boils down to a three-layer state machine. Understanding how data becomes a pixel requires looking at this closed loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Definition&lt;/strong&gt;: Defining the state and UI description (JSX / Widgets).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retention&lt;/strong&gt;: Persistent objects that manage state and logic (Fiber / Elements).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Computation&lt;/strong&gt;: The Diffing algorithm that calculates incremental updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumption&lt;/strong&gt;: The Rendering Engine that receives instructions and paints pixels.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-react-native-the-general-contractor-approach&quot;&gt;⚛️ React Native: The &quot;General Contractor&quot; Approach&lt;/h3&gt;
&lt;p&gt;React’s philosophy is to be the &lt;strong&gt;&quot;Brain of Declarative UI,&quot;&lt;/strong&gt; but it lacks its own &quot;hands.&quot; It manages the logic but outsources the actual work.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Layer &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Implementation &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Role &amp;amp; Constraints&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Description (JSX/TSX)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Blueprint Snapshot &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Immutable descriptions of &quot;what the UI should look like.&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Management (Fiber Node)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;React Core Dispatcher &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Manages state and calculates Diffs. All optimizations (&lt;code&gt;memo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;) live here.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Execution (Host Environment)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;OS / Browser Engine&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;React has no drawing power. It sends instructions to the host (DOM or Native UI).&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;the-bottleneck-the-foreign-agency-problem-&quot;&gt;The Bottleneck: &quot;The Foreign Agency Problem&quot;&lt;/h4&gt;
&lt;p&gt;React Native is a &lt;strong&gt;UI Manipulation Framework.&lt;/strong&gt; Because the actual components (Buttons, Lists) are rendered by the OS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High Communication Overhead&lt;/strong&gt;: Frequent updates (like high-speed scrolling) require constant &quot;bridging&quot; between the logic and the native UI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Black-Box Rendering&lt;/strong&gt;: Because rendering is delegated to Chrome or iOS/Android, React cannot intervene in &lt;em&gt;how&lt;/em&gt; a pixel is shaded. This is why complex, custom-path animations can feel &quot;heavy&quot; in RN.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;flutter-the-sovereign-ruler-approach&quot;&gt;Flutter: The &quot;Sovereign Ruler&quot; Approach&lt;/h3&gt;
&lt;p&gt;Flutter doesn’t trust the operating system’s components. It brings its own engine to the party. &lt;/p&gt;
&lt;p&gt;In Flutter, the &quot;Three-Tree&quot; architecture creates a full-stack closure from configuration to rasterization:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Layer &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Object &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Core Logic&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Blueprint (Configuration)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Widget&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Ultra-lightweight, immutable configurations created thousands of times per second.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Skeleton (Infrastructure)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Element&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;The bridge between the description and the rendered object; holds persistent state.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Rendering (Layout/Paint)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;RenderObject&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Flutter’s Secret Sauce.&lt;/strong&gt; It directly handles layout and painting instructions.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;the-advantage-rendering-sovereignty&quot;&gt;The Advantage: Rendering Sovereignty&lt;/h4&gt;
&lt;p&gt;Flutter utilizes its own rendering engine (evolving from Skia to &lt;strong&gt;Impeller&lt;/strong&gt;). This grants it &quot;Sovereign&quot; powers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Predictable Performance&lt;/strong&gt;: Since it doesn't rely on system components, the UI remains perfectly consistent across every device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficient Synthesis&lt;/strong&gt;: The transition from Widget → Element → RenderObject is highly optimized. Elements reuse RenderObjects whenever possible, drastically reducing the cost of expensive repaints.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-2026-decision-matrix-how-to-choose-&quot;&gt;The 2026 Decision Matrix: How to Choose?&lt;/h3&gt;
&lt;p&gt;We no longer choose based on &quot;community size.&quot; We choose based on &lt;strong&gt;Interactive Depth.&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;technical-comparison-matrix&quot;&gt;Technical Comparison Matrix&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Feature &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;React Native (Delegated Model) &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Flutter (Sovereign Model)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Rendering Granularity&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Component-level &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Pixel-level&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Pipeline Authority&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Delegated to Host &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Fully Autonomous&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Animation Ceiling&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Limited by Bridge efficiency &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Seamless and Precise&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Memory Footprint&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Lower (utilizes system UI) &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Higher (ships a full engine)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;strategic-recommendation&quot;&gt;Strategic Recommendation&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose Flutter if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You are building &lt;strong&gt;Performance-Intensive Apps&lt;/strong&gt;: Video editors, high-frequency data dashboards, or precision design tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual Consistency is Non-Negotiable&lt;/strong&gt;: Your design team demands pixel-perfect shadows and filters that look identical on a $200 Android and a $1200 iPhone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gamified UI&lt;/strong&gt;: Your app requires custom shaders or complex, fluid animations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose React Native if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You are building &lt;strong&gt;Standard Business Apps&lt;/strong&gt;: E-commerce, social media, or CRUD tools where native components are already &quot;good enough.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Ecosystem Integration&lt;/strong&gt;: You need to share heavy logic or hooks between your Web and Mobile teams.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Over-the-Air (OTA) Updates&lt;/strong&gt;: You prioritize the ability to push logic changes without a full App Store review cycle.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The Final Word&lt;/strong&gt;: Flutter is &lt;strong&gt;Building a World&lt;/strong&gt;, while React Native is &lt;strong&gt;Managing a World.&lt;/strong&gt; Understanding this distinction is the key to mastering the next era of cross-platform engineering.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://we1.top&quot;&gt;author&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://airouter.me&quot;&gt;web&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://nestifyapp.org&quot;&gt;Organize&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15806/the-physics-of-pixels-flutter-vs-react-native-in-2026</guid>
<pubDate>Sat, 25 Apr 2026 11:40:52 +0000</pubDate>
</item>
<item>
<title>Gradle build errors in React Native: what usually wastes the most time</title>
<link>https://coderlegion.com/14096/gradle-build-errors-in-react-native-what-usually-wastes-the-most-time</link>
<description>&lt;p&gt;If a React Native Android build fails and the output mentions Gradle, it is very easy to lose time in the wrong place.&lt;/p&gt;
&lt;p&gt;The word “Gradle” is so broad that it can hide very different root causes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;version mismatches&lt;/li&gt;
&lt;li&gt;plugin resolution failures&lt;/li&gt;
&lt;li&gt;dependency conflicts&lt;/li&gt;
&lt;li&gt;autolinking problems&lt;/li&gt;
&lt;li&gt;corrupted caches&lt;/li&gt;
&lt;li&gt;environment issues on the machine&lt;/li&gt;
&lt;li&gt;native setup mistakes after upgrades&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is why “Gradle build error” is often less of a diagnosis and more of a category.&lt;/p&gt;
&lt;p&gt;I’ve seen this kind of issue enough times to notice a pattern: the biggest time loss usually does not come from the bug itself. It comes from checking the wrong things first.&lt;/p&gt;
&lt;p&gt;This is the checklist I would use to avoid that.&lt;/p&gt;
&lt;h3 id=&quot;the-first-trap-treating-gradle-as-the-actual-problem&quot;&gt;The first trap: treating Gradle as the actual problem&lt;/h3&gt;
&lt;p&gt;When people say “I have a Gradle error,” they usually mean one of two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the Android build failed somewhere inside the Gradle process&lt;/li&gt;
&lt;li&gt;the terminal output included a Gradle task name, plugin message, or stacktrace&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; automatically mean Gradle itself is the real root cause.&lt;/p&gt;
&lt;p&gt;Very often, the actual issue is one of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React Native / AGP / Kotlin / JDK versions do not align&lt;/li&gt;
&lt;li&gt;a plugin cannot be resolved&lt;/li&gt;
&lt;li&gt;a native library was added but not integrated correctly&lt;/li&gt;
&lt;li&gt;autolinking did not generate what the project expects&lt;/li&gt;
&lt;li&gt;some Firebase / Android dependency is conflicting with another one&lt;/li&gt;
&lt;li&gt;local machine setup is broken&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the first mental correction I make is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gradle is the place where the failure appears. It is not always the thing that is actually wrong.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-usually-wastes-the-most-time&quot;&gt;What usually wastes the most time&lt;/h3&gt;
&lt;h4 id=&quot;1-reading-the-last-line-instead-of-the-first-useful-one&quot;&gt;1. Reading the last line instead of the first useful one&lt;/h4&gt;
&lt;p&gt;One of the most common wastes of time is staring at the bottom of the output.&lt;/p&gt;
&lt;p&gt;The last line usually tells you that the build failed. It does not tell you why.&lt;/p&gt;
&lt;p&gt;The useful clue is often earlier:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the first plugin that could not be found&lt;/li&gt;
&lt;li&gt;the first unresolved dependency&lt;/li&gt;
&lt;li&gt;the first incompatible version&lt;/li&gt;
&lt;li&gt;the first task that failed for a concrete reason&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I want to save time, I search for the &lt;strong&gt;first meaningful error&lt;/strong&gt;, not the most dramatic-looking one at the end.&lt;/p&gt;
&lt;h3 id=&quot;2-deleting-everything-before-understanding-the-category-of-failure&quot;&gt;2. Deleting everything before understanding the category of failure&lt;/h3&gt;
&lt;p&gt;Clearing caches can help sometimes.&lt;/p&gt;
&lt;p&gt;But many people immediately do all of this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;delete &lt;code&gt;.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;clean the project&lt;/li&gt;
&lt;li&gt;reinstall node modules&lt;/li&gt;
&lt;li&gt;reinstall Android Studio&lt;/li&gt;
&lt;li&gt;reinstall the JDK&lt;/li&gt;
&lt;li&gt;reboot the machine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes that works. But often it just burns time and makes the debugging less focused.&lt;/p&gt;
&lt;p&gt;I only treat it as a cache/environment cleanup issue if the error actually points in that direction, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;corrupted transforms&lt;/li&gt;
&lt;li&gt;temporary workspace move failures&lt;/li&gt;
&lt;li&gt;stale artifacts&lt;/li&gt;
&lt;li&gt;file locking&lt;/li&gt;
&lt;li&gt;download/certificate issues&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the problem is really version alignment or plugin resolution, deleting everything does not solve the core issue.&lt;/p&gt;
&lt;h3 id=&quot;3-ignoring-version-compatibility-across-the-android-toolchain&quot;&gt;3. Ignoring version compatibility across the Android toolchain&lt;/h3&gt;
&lt;p&gt;A lot of Gradle build errors in React Native are really compatibility problems between these pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React Native version&lt;/li&gt;
&lt;li&gt;Gradle wrapper version&lt;/li&gt;
&lt;li&gt;Android Gradle Plugin version&lt;/li&gt;
&lt;li&gt;Kotlin version&lt;/li&gt;
&lt;li&gt;JDK version&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If one of those is too old, too new, or mismatched with the others, the build can fail even if your app code is completely fine.&lt;/p&gt;
&lt;p&gt;This is especially likely after:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upgrading React Native&lt;/li&gt;
&lt;li&gt;copying Android config from another project&lt;/li&gt;
&lt;li&gt;updating Firebase/native libraries&lt;/li&gt;
&lt;li&gt;mixing Expo-related config with bare React Native setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the error looks structural, version alignment is one of the first things I check.&lt;/p&gt;
&lt;h3 id=&quot;4-looking-only-at-dependencies-and-ignoring-settings-gradle&quot;&gt;4. Looking only at dependencies and ignoring &lt;code&gt;settings.gradle&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Some failures are not about package versions at all.&lt;/p&gt;
&lt;p&gt;They are about plugin resolution and build structure.&lt;/p&gt;
&lt;p&gt;If the error mentions things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;plugin not found&lt;/li&gt;
&lt;li&gt;included build not found&lt;/li&gt;
&lt;li&gt;configuration of the root project failed&lt;/li&gt;
&lt;li&gt;React Native Gradle plugin issues&lt;/li&gt;
&lt;li&gt;autolinking/setup task problems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;then I go straight to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;android/settings.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;root &lt;code&gt;build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;plugin management / includeBuild setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters a lot in newer React Native Android projects, where small mistakes in how the plugin is included can break the whole build.&lt;/p&gt;
&lt;h3 id=&quot;5-forgetting-what-changed-last&quot;&gt;5. Forgetting what changed last&lt;/h3&gt;
&lt;p&gt;This is one of the highest-value questions in debugging:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What changed right before the build started failing?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;added a native package&lt;/li&gt;
&lt;li&gt;updated React Native&lt;/li&gt;
&lt;li&gt;updated Firebase packages&lt;/li&gt;
&lt;li&gt;changed Android config&lt;/li&gt;
&lt;li&gt;changed Kotlin/Gradle/JDK versions&lt;/li&gt;
&lt;li&gt;modified signing or release setup&lt;/li&gt;
&lt;li&gt;switched Expo/native configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A lot of time gets wasted when the current error is treated as if it appeared randomly.&lt;/p&gt;
&lt;p&gt;Usually it didn’t. Usually something moved.&lt;/p&gt;
&lt;p&gt;The shortest path is often to inspect the most recent meaningful change.&lt;/p&gt;
&lt;h3 id=&quot;6-blaming-firebase-too-early-when-the-issue-is-broader&quot;&gt;6. Blaming Firebase too early when the issue is broader&lt;/h3&gt;
&lt;p&gt;If the problem started after adding or changing Firebase, it is tempting to assume the whole issue is “a Firebase bug.”&lt;/p&gt;
&lt;p&gt;But many Firebase-related Gradle failures are really one of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependency mismatch&lt;/li&gt;
&lt;li&gt;version conflict&lt;/li&gt;
&lt;li&gt;plugin/config issue&lt;/li&gt;
&lt;li&gt;native setup not completed&lt;/li&gt;
&lt;li&gt;packaging conflict&lt;/li&gt;
&lt;li&gt;outdated Android build setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So even when Firebase appears in the output, I still treat the build as a full Android build-chain problem until proven otherwise.&lt;/p&gt;
&lt;h3 id=&quot;7-treating-all-native-modules-as-if-they-install-the-same-way&quot;&gt;7. Treating all native modules as if they install the same way&lt;/h3&gt;
&lt;p&gt;React Native projects can get fragile when native modules accumulate.&lt;/p&gt;
&lt;p&gt;Some libraries rely cleanly on autolinking. Others still depend on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;correct plugin setup&lt;/li&gt;
&lt;li&gt;Android manifest changes&lt;/li&gt;
&lt;li&gt;Gradle config&lt;/li&gt;
&lt;li&gt;native initialization&lt;/li&gt;
&lt;li&gt;version-specific setup steps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the build started failing after adding a package, I do not just ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Did I install it?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What exact Android expectations did this package add?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That distinction saves a lot of guessing.&lt;/p&gt;
&lt;h3 id=&quot;8-confusing-machine-setup-problems-with-project-problems&quot;&gt;8. Confusing machine/setup problems with project problems&lt;/h3&gt;
&lt;p&gt;Some Gradle errors are about the project.&lt;/p&gt;
&lt;p&gt;Some are about the machine.&lt;/p&gt;
&lt;p&gt;That is a major distinction.&lt;/p&gt;
&lt;p&gt;I think “machine/setup issue” when the failure points toward things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;missing SDK components&lt;/li&gt;
&lt;li&gt;JDK problems&lt;/li&gt;
&lt;li&gt;PATH/environment issues&lt;/li&gt;
&lt;li&gt;SSL/certificate/download failures&lt;/li&gt;
&lt;li&gt;local permissions&lt;/li&gt;
&lt;li&gt;file locking on Windows&lt;/li&gt;
&lt;li&gt;Gradle daemon state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the project build files look normal but dependency downloads fail or local tools are inconsistent, I stop debugging app code and start checking the environment.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-mental-model&quot;&gt;A practical mental model&lt;/h3&gt;
&lt;p&gt;When a React Native Android build fails with a Gradle-heavy error, I sort it into one of these buckets first.&lt;/p&gt;
&lt;h4 id=&quot;bucket-1-build-toolchain-mismatch&quot;&gt;Bucket 1: Build toolchain mismatch&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Gradle wrapper&lt;/li&gt;
&lt;li&gt;AGP&lt;/li&gt;
&lt;li&gt;Kotlin&lt;/li&gt;
&lt;li&gt;JDK&lt;/li&gt;
&lt;li&gt;React Native version alignment&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;bucket-2-build-structure-plugin-resolution&quot;&gt;Bucket 2: Build structure / plugin resolution&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;settings.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;plugin management&lt;/li&gt;
&lt;li&gt;includeBuild&lt;/li&gt;
&lt;li&gt;plugin not found&lt;/li&gt;
&lt;li&gt;autolinking structure&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;bucket-3-native-dependency-integration&quot;&gt;Bucket 3: Native dependency integration&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;newly added packages&lt;/li&gt;
&lt;li&gt;Firebase native setup&lt;/li&gt;
&lt;li&gt;Expo/native mix&lt;/li&gt;
&lt;li&gt;Android-specific install expectations&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;bucket-4-cache-local-environment&quot;&gt;Bucket 4: Cache / local environment&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;corrupted &lt;code&gt;.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;locked files&lt;/li&gt;
&lt;li&gt;SDK/JDK issues&lt;/li&gt;
&lt;li&gt;SSL/download failures&lt;/li&gt;
&lt;li&gt;local machine inconsistency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is not to know the exact fix instantly.&lt;/p&gt;
&lt;p&gt;The goal is to identify the correct category as early as possible.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-not-do-first&quot;&gt;What I would not do first&lt;/h3&gt;
&lt;p&gt;I would not immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rewrite multiple Gradle files at once&lt;/li&gt;
&lt;li&gt;downgrade random packages&lt;/li&gt;
&lt;li&gt;reinstall everything on the machine&lt;/li&gt;
&lt;li&gt;clear every cache without reason&lt;/li&gt;
&lt;li&gt;assume the word “Gradle” is the real root cause&lt;/li&gt;
&lt;li&gt;ignore the last meaningful project change&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That usually increases noise instead of reducing it.&lt;/p&gt;
&lt;h3 id=&quot;my-practical-order-of-checks&quot;&gt;My practical order of checks&lt;/h3&gt;
&lt;p&gt;If I hit a React Native Android Gradle build error, this is the order I would usually follow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;find the first meaningful error in the build output&lt;/li&gt;
&lt;li&gt;identify which category the failure belongs to&lt;/li&gt;
&lt;li&gt;check RN / Gradle / AGP / Kotlin / JDK compatibility&lt;/li&gt;
&lt;li&gt;inspect &lt;code&gt;settings.gradle&lt;/code&gt; and plugin resolution if the error is structural&lt;/li&gt;
&lt;li&gt;review the last package or config change&lt;/li&gt;
&lt;li&gt;check native module integration expectations&lt;/li&gt;
&lt;li&gt;only then treat it as a cache/environment cleanup problem if the error supports that direction&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That order usually saves more time than any specific trick.&lt;/p&gt;
&lt;h3 id=&quot;final-takeaway&quot;&gt;Final takeaway&lt;/h3&gt;
&lt;p&gt;The phrase “Gradle build error” often wastes time because it sounds more specific than it really is.&lt;/p&gt;
&lt;p&gt;Most of the real causes fall into repeatable patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;version mismatch&lt;/li&gt;
&lt;li&gt;plugin/build structure problem&lt;/li&gt;
&lt;li&gt;native integration issue&lt;/li&gt;
&lt;li&gt;cache/environment issue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The faster you identify the category, the faster the fix becomes possible.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14096/gradle-build-errors-in-react-native-what-usually-wastes-the-most-time</guid>
<pubDate>Mon, 06 Apr 2026 16:49:16 +0000</pubDate>
</item>
<item>
<title>React Native Android build failed: what I would check first</title>
<link>https://coderlegion.com/14095/react-native-android-build-failed-what-i-would-check-first</link>
<description>&lt;p&gt;If a React Native Android build suddenly fails, the error message is often much less helpful than it looks.&lt;/p&gt;
&lt;p&gt;Sometimes it points to Gradle, sometimes to a plugin, sometimes to a dependency, and sometimes it just fails deep inside the Android build process without making the real cause obvious.&lt;/p&gt;
&lt;p&gt;I’ve run into this kind of problem enough times to stop treating “build failed” as one issue.&lt;/p&gt;
&lt;p&gt;It is usually a category of issues.&lt;/p&gt;
&lt;p&gt;This is the checklist I would go through first before changing random files or reinstalling everything.&lt;/p&gt;
&lt;h3 id=&quot;first-treat-build-failures-as-clues-not-conclusions&quot;&gt;First: treat build failures as clues, not conclusions&lt;/h3&gt;
&lt;p&gt;A React Native Android build can fail because of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gradle configuration issues&lt;/li&gt;
&lt;li&gt;plugin resolution problems&lt;/li&gt;
&lt;li&gt;dependency/version mismatches&lt;/li&gt;
&lt;li&gt;Kotlin or AGP incompatibility&lt;/li&gt;
&lt;li&gt;corrupted caches&lt;/li&gt;
&lt;li&gt;autolinking problems&lt;/li&gt;
&lt;li&gt;native module integration issues&lt;/li&gt;
&lt;li&gt;environment/setup problems on the machine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means the text &lt;code&gt;BUILD FAILED&lt;/code&gt; is not the diagnosis. It is just the start of the diagnosis.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-check-first&quot;&gt;What I would check first&lt;/h3&gt;
&lt;h4 id=&quot;1-read-the-first-meaningful-error-not-the-last-line&quot;&gt;1. Read the first meaningful error, not the last line&lt;/h4&gt;
&lt;p&gt;One of the easiest mistakes is focusing on the bottom of the terminal output.&lt;/p&gt;
&lt;p&gt;The last line usually just says the build failed. The useful part is often earlier:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the first missing plugin&lt;/li&gt;
&lt;li&gt;the first unresolved dependency&lt;/li&gt;
&lt;li&gt;the first incompatible version&lt;/li&gt;
&lt;li&gt;the first task that actually broke&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I see a huge wall of Gradle output, I try to find the first message that clearly indicates what could not be found, resolved, or configured.&lt;/p&gt;
&lt;p&gt;That usually narrows the problem much faster.&lt;/p&gt;
&lt;h3 id=&quot;2-check-react-native-gradle-agp-kotlin-compatibility&quot;&gt;2. Check React Native / Gradle / AGP / Kotlin compatibility&lt;/h3&gt;
&lt;p&gt;A lot of Android build failures are really version alignment problems.&lt;/p&gt;
&lt;p&gt;I would check whether these pieces actually make sense together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React Native version&lt;/li&gt;
&lt;li&gt;Gradle wrapper version&lt;/li&gt;
&lt;li&gt;Android Gradle Plugin version&lt;/li&gt;
&lt;li&gt;Kotlin version&lt;/li&gt;
&lt;li&gt;installed JDK version&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A mismatch here can break the build even when your app code is fine.&lt;/p&gt;
&lt;p&gt;This is especially common after:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upgrading React Native&lt;/li&gt;
&lt;li&gt;adding or updating native libraries&lt;/li&gt;
&lt;li&gt;copying build config from another project&lt;/li&gt;
&lt;li&gt;mixing Expo/native config changes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-look-at-settings-gradle-and-plugin-resolution-first&quot;&gt;3. Look at &lt;code&gt;settings.gradle&lt;/code&gt; and plugin resolution first&lt;/h3&gt;
&lt;p&gt;If the error mentions plugins not being found, configuration failing, or included builds not resolving correctly, I would go straight to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;android/settings.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;root &lt;code&gt;build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;plugin management / includeBuild setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In newer React Native setups, especially around the Gradle plugin and autolinking, small mistakes there can break the entire build.&lt;/p&gt;
&lt;p&gt;This is one of the first places I would inspect if the error looks structural instead of library-specific.&lt;/p&gt;
&lt;h3 id=&quot;4-check-whether-this-is-really-a-cache-problem&quot;&gt;4. Check whether this is really a cache problem&lt;/h3&gt;
&lt;p&gt;Sometimes it is. Sometimes it is not.&lt;/p&gt;
&lt;p&gt;I would only treat it as a cache issue when the error actually suggests things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;temp workspace move failures&lt;/li&gt;
&lt;li&gt;corrupted transforms&lt;/li&gt;
&lt;li&gt;unexpected stale build behavior&lt;/li&gt;
&lt;li&gt;dependency artifacts behaving inconsistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In that case, I would think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gradle daemon state&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gradle&lt;/code&gt; cache corruption&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android/.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android/build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;whether Android Studio or another process is holding locks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But I would not start by deleting everything automatically unless the error really points that way.&lt;/p&gt;
&lt;h3 id=&quot;5-check-native-dependencies-and-recently-added-packages&quot;&gt;5. Check native dependencies and recently added packages&lt;/h3&gt;
&lt;p&gt;If the build worked before and stopped working after adding or updating something, that is one of the strongest signals you have.&lt;/p&gt;
&lt;p&gt;I would ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what changed last?&lt;/li&gt;
&lt;li&gt;did I add a native dependency?&lt;/li&gt;
&lt;li&gt;did I update Firebase packages?&lt;/li&gt;
&lt;li&gt;did I change Expo/native modules?&lt;/li&gt;
&lt;li&gt;did I update React Native or Gradle-related files?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That often gives a faster answer than staring at logs in isolation.&lt;/p&gt;
&lt;h3 id=&quot;6-watch-for-autolinking-and-native-module-issues&quot;&gt;6. Watch for autolinking and native module issues&lt;/h3&gt;
&lt;p&gt;React Native Android builds often fail because a native module was not linked/configured the way the project expects.&lt;/p&gt;
&lt;p&gt;This gets more likely when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mixing Expo modules with bare React Native config&lt;/li&gt;
&lt;li&gt;using libraries with native setup steps&lt;/li&gt;
&lt;li&gt;upgrading React Native without updating supporting config&lt;/li&gt;
&lt;li&gt;relying on autolinking that did not actually complete properly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the error mentions package resolution, missing generated files, or native tasks around module setup, I would treat autolinking as a serious suspect.&lt;/p&gt;
&lt;h3 id=&quot;7-distinguish-machine-setup-problems-from-project-problems&quot;&gt;7. Distinguish machine/setup problems from project problems&lt;/h3&gt;
&lt;p&gt;Sometimes the project is broken.&lt;/p&gt;
&lt;p&gt;Sometimes the machine is.&lt;/p&gt;
&lt;p&gt;That is an important difference.&lt;/p&gt;
&lt;p&gt;I would think about machine/environment issues when the error points toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK problems&lt;/li&gt;
&lt;li&gt;missing Android SDK components&lt;/li&gt;
&lt;li&gt;PATH / environment variable issues&lt;/li&gt;
&lt;li&gt;SSL / certificate / dependency download failures&lt;/li&gt;
&lt;li&gt;local file locking or permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is why a build issue can appear “random” even when the project itself has not changed much.&lt;/p&gt;
&lt;h3 id=&quot;8-if-firebase-is-involved-do-not-assume-it-is-only-a-firebase-issue&quot;&gt;8. If Firebase is involved, do not assume it is only a Firebase issue&lt;/h3&gt;
&lt;p&gt;Firebase-related packages can surface as Android build failures even when the deeper cause is one of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependency mismatch&lt;/li&gt;
&lt;li&gt;outdated native setup&lt;/li&gt;
&lt;li&gt;plugin/version conflict&lt;/li&gt;
&lt;li&gt;Gradle packaging issue&lt;/li&gt;
&lt;li&gt;release/debug configuration difference&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So if the failure starts after adding Firebase or changing Firebase setup, I would still inspect the Android build chain broadly instead of narrowing too fast.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-mental-model-1&quot;&gt;A practical mental model&lt;/h3&gt;
&lt;p&gt;When I see “React Native Android build failed,” I usually sort the possibilities into these buckets:&lt;/p&gt;
&lt;h4 id=&quot;bucket-1-build-system&quot;&gt;Bucket 1: Build system&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Gradle&lt;/li&gt;
&lt;li&gt;AGP&lt;/li&gt;
&lt;li&gt;Kotlin&lt;/li&gt;
&lt;li&gt;plugin management&lt;/li&gt;
&lt;li&gt;wrapper version&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;bucket-2-native-integration&quot;&gt;Bucket 2: Native integration&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;autolinking&lt;/li&gt;
&lt;li&gt;native modules&lt;/li&gt;
&lt;li&gt;Expo/native mix&lt;/li&gt;
&lt;li&gt;Firebase native setup&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;bucket-3-local-environment&quot;&gt;Bucket 3: Local environment&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;JDK&lt;/li&gt;
&lt;li&gt;SDK&lt;/li&gt;
&lt;li&gt;PATH&lt;/li&gt;
&lt;li&gt;cache corruption&lt;/li&gt;
&lt;li&gt;SSL/certificate/download issues&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That mental model helps me avoid random debugging.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-not-do-first-1&quot;&gt;What I would not do first&lt;/h3&gt;
&lt;p&gt;I would not immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reinstall the whole machine setup&lt;/li&gt;
&lt;li&gt;randomly edit multiple Gradle files&lt;/li&gt;
&lt;li&gt;downgrade several libraries at once&lt;/li&gt;
&lt;li&gt;clear every cache without understanding the error&lt;/li&gt;
&lt;li&gt;assume the last line in the terminal is the root cause&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That usually creates more noise.&lt;/p&gt;
&lt;h3 id=&quot;my-practical-order-of-checks-1&quot;&gt;My practical order of checks&lt;/h3&gt;
&lt;p&gt;If a React Native Android build fails, this is the order I would usually go through:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;find the first meaningful error in the output&lt;/li&gt;
&lt;li&gt;check version compatibility across RN / Gradle / AGP / Kotlin / JDK&lt;/li&gt;
&lt;li&gt;inspect &lt;code&gt;settings.gradle&lt;/code&gt; and plugin resolution&lt;/li&gt;
&lt;li&gt;review the most recent package or config change&lt;/li&gt;
&lt;li&gt;check native module/autolinking issues&lt;/li&gt;
&lt;li&gt;only then treat it as cache/environment cleanup if the error points there&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That order tends to save time.&lt;/p&gt;
&lt;h3 id=&quot;final-takeaway-1&quot;&gt;Final takeaway&lt;/h3&gt;
&lt;p&gt;“React Native Android build failed” is not one problem.&lt;/p&gt;
&lt;p&gt;It is usually one of a few repeated categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;version mismatch&lt;/li&gt;
&lt;li&gt;plugin/config problem&lt;/li&gt;
&lt;li&gt;native integration issue&lt;/li&gt;
&lt;li&gt;cache/environment issue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The faster you identify which category you are in, the faster the build becomes fixable.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14095/react-native-android-build-failed-what-i-would-check-first</guid>
<pubDate>Mon, 06 Apr 2026 16:47:53 +0000</pubDate>
</item>
<item>
<title>Flutter vs React Native AI Apps: My 2026 Take</title>
<link>https://coderlegion.com/14039/flutter-vs-react-native-ai-apps-my-2026-take</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buildzn.com/blog/flutter-vs-react-native-ai-apps-my-2026-take&quot;&gt;BuildZn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everyone talks about how AI will change mobile, but nobody explains which tech stack actually gets you there without burning through your seed round. You’re Googling &lt;strong&gt;Flutter vs React Native AI apps&lt;/strong&gt; right now because you need clarity, not marketing fluff. I spent a solid week building a proof-of-concept for an on-device AI feature last month, wrestling with both frameworks. Here's what actually works, and where each framework shines (or trips).&lt;/p&gt;
&lt;h3 id=&quot;why-ai-powered-mobile-apps-aren-t-just-hype-anymore&quot;&gt;Why AI-Powered Mobile Apps Aren't Just Hype Anymore&lt;/h3&gt;
&lt;p&gt;Look, in 2026, if your mobile app isn't at least thinking about AI, it's already behind. It's not about chatbots anymore; it’s about &lt;strong&gt;AI-powered mobile app development&lt;/strong&gt; driving real user value and efficiency. Think personalized content feeds, real-time object recognition, intelligent automation, or even predictive analytics directly on the user's phone.&lt;/p&gt;
&lt;p&gt;We’re past the experimental phase. Users expect smart features. Founders, you need to decide if you’re building a product that just &lt;em&gt;uses&lt;/em&gt; AI via an API call to OpenAI, or if you're embedding complex machine learning models directly into the app for speed, offline capability, and better data privacy. This choice dictates a lot about your framework, cost, and overall strategy.&lt;/p&gt;
&lt;h3 id=&quot;flutter-vs-react-native-ai-apps-the-real-deal-for-your-wallet-and-timeline&quot;&gt;Flutter vs React Native AI Apps: The Real Deal for Your Wallet and Timeline&lt;/h3&gt;
&lt;p&gt;Alright, let's get into the brass tacks. You care about cost, time to market, and how well this thing will perform when it's handling real AI tasks. My experience across over 20 production apps – including FarahGPT with its 5,100+ users and a complex gold trading system – gives me a pretty clear view.&lt;/p&gt;
&lt;p&gt;Here’s the breakdown:&lt;/p&gt;
&lt;h4 id=&quot;flutter-for-ai-powered-apps&quot;&gt;Flutter for AI-Powered Apps&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance:&lt;/strong&gt; This is where Flutter often wins, hands down, for serious on-device AI. Because it compiles directly to native code, you get near-native performance. When you’re running a TensorFlow Lite model for real-time image processing or audio analysis, those milliseconds matter. The UI stays smooth even when the CPU is churning through ML inferences.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development Speed &amp;amp; Cost:&lt;/strong&gt; My team ships fast with Flutter. A single codebase for iOS and Android means half the development effort compared to building two separate native apps. For &lt;strong&gt;mobile app framework AI 2026&lt;/strong&gt; discussions, this translates directly into &lt;strong&gt;lower development costs and faster timelines&lt;/strong&gt;. One team, one codebase, one set of AI integrations. Simpler to maintain too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Integration:&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On-Device ML:&lt;/strong&gt; Flutter's integration with TensorFlow Lite is solid. You drop your &lt;code&gt;.tflite&lt;/code&gt; model, use the &lt;code&gt;tflite_flutter&lt;/code&gt; package, and you're good. For platform-specific features like Apple's Core ML or Google's ML Kit, Flutter's FFI (Foreign Function Interface) or platform channels let you tap into native capabilities directly with minimal boilerplate. Honestly, this direct access is a huge advantage for complex, performance-critical AI tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud-Based AI:&lt;/strong&gt; For calling APIs like OpenAI, Google Gemini, or custom ML inference services, both frameworks are equally capable. It’s just standard HTTP requests, which Flutter handles efficiently.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;react-native-for-ai-powered-apps&quot;&gt;React Native for AI-Powered Apps&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ecosystem &amp;amp; Flexibility:&lt;/strong&gt; React Native rides on the massive JavaScript ecosystem. If you’re already heavy into web development and have JS talent, the learning curve is shallower. There are plenty of existing JS libraries, and some bridge to native AI SDKs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance:&lt;/strong&gt; This is where it gets tricky. For basic API calls to cloud AI, React Native is fine. But for intensive &lt;strong&gt;on-device machine learning apps&lt;/strong&gt;, the JavaScript bridge can introduce overhead. When you need to process large datasets or run complex models in real-time, that bridge can become a bottleneck. You often end up writing native modules (Java/Kotlin for Android, Swift/Objective-C for iOS) for performance-critical parts, which defeats the &quot;single codebase&quot; promise and drives up costs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development Speed &amp;amp; Cost:&lt;/strong&gt; Initial setup can be quick if you're familiar with React. However, once you hit native feature requirements or performance bottlenecks for AI, development slows down. You need developers proficient in React Native &lt;em&gt;and&lt;/em&gt; native iOS/Android, effectively increasing your team size or skill requirements. This can lead to higher long-term maintenance costs and longer development cycles for complex &lt;strong&gt;AI-powered mobile app development&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Integration:&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On-Device ML:&lt;/strong&gt; Requires finding or building native modules that expose TensorFlow Lite, Core ML, or ML Kit to JavaScript. While packages exist (e.g., &lt;code&gt;react-native-tflite-camera&lt;/code&gt;), they often lag behind native SDK updates or have limitations. When you need something cutting-edge, you might be building a lot of custom native code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud-Based AI:&lt;/strong&gt; Just like Flutter, React Native handles API calls to cloud AI services perfectly well using standard libraries like Axios or Fetch.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;so-who-wins-for-ai-&quot;&gt;So, who wins for AI?&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;My take: If your AI strategy leans heavily on custom, on-device machine learning models for real-time processing or offline capabilities, Flutter is the stronger choice.&lt;/strong&gt; The performance advantage and simpler native integration pathways are a game-changer. &lt;strong&gt;If your AI is primarily cloud-based (API calls) and you have an existing JS team, React Native can work, but be prepared for potential hurdles if you later need serious on-device ML.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For founders, this translates to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flutter = More predictable costs, faster iteration for complex AI, better performance ceilings.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React Native = Potentially faster initial setup if JS-heavy, but higher risk of costly native refactors for advanced AI needs.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-ai-integration-actually-looks-not-just-theory-&quot;&gt;How AI Integration Actually Looks (Not Just Theory)&lt;/h3&gt;
&lt;p&gt;When we built FarahGPT, the core logic was entirely backend. The mobile app (Flutter, naturally) just sent user input to our Node.js API, which then talked to OpenAI. Simple network calls. That’s a common, effective strategy for many &lt;strong&gt;best framework for AI startups&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But what if you're building something else?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloud-Based AI (API Calls):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is the easiest path. Your mobile app sends data (text, image, audio) to a backend service (your Node.js API, or a direct call to Google Gemini/AWS Rekognition).&lt;/li&gt;
&lt;li&gt;The backend processes it, uses an AI model, and sends the result back to the app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Both Flutter and React Native handle this beautifully.&lt;/strong&gt; It's just HTTP requests. The choice here comes down to your general mobile app strategy, not AI specific issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Example: Flutter calling an AI API
Future&amp;lt;String&amp;gt; getAIResponse(String prompt) async {
  final response = await http.post(
    Uri.parse('https://api.farahgpt.com/generate'),
    headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_KEY'},
    body: jsonEncode({'text': prompt}),
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body)['response'];
  } else {
    throw Exception('Failed to get AI response: ${response.body}');
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code is straightforward. React Native would look similar with &lt;code&gt;fetch&lt;/code&gt; or &lt;code&gt;axios&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;On-Device AI (Local Models):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is for scenarios where you need:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Real-time processing:&lt;/strong&gt; Image filters, speech-to-text, object detection that can't wait for network latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline functionality:&lt;/strong&gt; AI features work without an internet connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data privacy:&lt;/strong&gt; Sensitive data never leaves the device.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This is where Flutter truly shines for &lt;strong&gt;Flutter for machine learning apps&lt;/strong&gt;. With TensorFlow Lite, you embed the model directly. For example, in my gold trading system, some initial data validation and pattern recognition happened on-device before hitting the main backend.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flutter Implementation:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;tflite_flutter&lt;/code&gt; to your &lt;code&gt;pubspec.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Place your &lt;code&gt;.tflite&lt;/code&gt; model in your assets.&lt;/li&gt;
&lt;li&gt;Load the model and run inferences.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;React Native Implementation:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Requires &lt;code&gt;react-native-tflite-camera&lt;/code&gt; or similar, which might involve linking native modules.&lt;/li&gt;
&lt;li&gt;Often involves more complex setup and potential versioning issues with native SDKs.&lt;/li&gt;
&lt;li&gt;Performance-sensitive parts might still push you to write custom native modules.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Here's the thing — don't overcomplicate it if you don't have to.&lt;/strong&gt; Most startups can start with cloud-based AI. Only go on-device if you absolutely need the speed, offline, or privacy benefits.&lt;/p&gt;
&lt;h3 id=&quot;what-most-founders-get-wrong-about-ai-apps&quot;&gt;What Most Founders Get Wrong About AI Apps&lt;/h3&gt;
&lt;p&gt;I've seen founders waste a lot of cash and time because of some common misunderstandings. Don't be that founder.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mistake 1: Assuming &quot;AI&quot; means everything happens on the phone.&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Reality:&lt;/em&gt; For 90% of use cases, your app just sends data to a powerful AI model running in the cloud. Think about how ChatGPT works – your phone isn't running a large language model. It's hitting an API. Focus on a robust backend API strategy first.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mistake 2: Underestimating the data required.&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Reality:&lt;/em&gt; Your AI model is only as good as the data it's trained on. If you're building a custom AI feature, collecting and curating quality data is often 10x harder than picking a framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mistake 3: Ignoring long-term maintenance costs for native modules.&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Reality:&lt;/em&gt; If you pick React Native but end up writing a lot of custom native code for AI, you now have three codebases to maintain (JS, iOS Native, Android Native). Updates, bug fixes, and feature enhancements become much more expensive. &lt;strong&gt;Flutter generally avoids this trap by handling native interactions more gracefully.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mistake 4: Not factoring in security and privacy from day one.&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Reality:&lt;/em&gt; If your AI processes sensitive user data, you need to think about encryption, data anonymization, and compliance (GDPR, HIPAA, etc.). This applies to both on-device and cloud AI. Your framework choice won't fix poor security architecture.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;optimizing-your-ai-app-beyond-the-framework&quot;&gt;Optimizing Your AI App: Beyond the Framework&lt;/h3&gt;
&lt;p&gt;Picking between &lt;strong&gt;Flutter vs React Native AI apps&lt;/strong&gt; is just one piece of the puzzle. To build a truly performant, scalable, and secure AI app, you need to consider the whole stack.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Backend Choice:&lt;/strong&gt; For AI APIs, I often lean on Node.js. It's fast, scalable, and great for handling concurrent requests to external AI services or your own custom models. My experience building the backend for FarahGPT and the gold trading system confirms this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud AI Services:&lt;/strong&gt; Don't reinvent the wheel. Google Cloud ML Kit, AWS Rekognition/SageMaker, Azure AI services – these provide pre-trained models and managed infrastructure that can save you years of development. Use them, especially in the early stages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Security &amp;amp; Compliance:&lt;/strong&gt; Crucial for any app, but especially for AI. Encrypt data in transit and at rest. If processing sensitive info, ensure your backend infrastructure meets compliance standards.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability:&lt;/strong&gt; If your AI app takes off (and hopefully it does!), your backend needs to scale. Plan for serverless functions (AWS Lambda, Google Cloud Functions) or containerized services (Kubernetes) to handle increased AI inference requests.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;faqs&quot;&gt;FAQs&lt;/h3&gt;
&lt;h4 id=&quot;q1-which-framework-is-cheaper-for-an-ai-startup-&quot;&gt;Q1: Which framework is cheaper for an AI startup?&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; For basic cloud AI, costs are similar. For complex on-device AI, &lt;strong&gt;Flutter often offers lower overall TCO&lt;/strong&gt; due to better performance, fewer native development needs, and thus, fewer specialized developers required.&lt;/p&gt;
&lt;h4 id=&quot;q2-can-i-use-existing-ai-models-with-flutter-react-native-&quot;&gt;Q2: Can I use existing AI models with Flutter/React Native?&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; Yes, both can consume cloud AI APIs (like OpenAI, Google Gemini). For on-device, Flutter integrates well with TensorFlow Lite directly. React Native typically requires community packages or custom native modules for local ML.&lt;/p&gt;
&lt;h4 id=&quot;q3-what-if-i-need-super-fast-real-time-ai-processing-&quot;&gt;Q3: What if I need super fast, real-time AI processing?&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; If low latency and high throughput are critical, &lt;strong&gt;Flutter is generally preferred for its near-native performance&lt;/strong&gt; when running on-device models. This reduces reliance on network speeds and minimizes UI jank.&lt;/p&gt;
&lt;p&gt;Look, by 2026, every app will have some AI baked in. The choice of &lt;strong&gt;mobile app framework AI 2026&lt;/strong&gt; is critical. My strong opinion? For a serious &lt;strong&gt;AI-powered mobile app development&lt;/strong&gt; project, especially if you foresee needing any significant on-device machine learning or truly custom experiences, &lt;strong&gt;Flutter is the clear winner for its performance, developer efficiency, and future-proofing.&lt;/strong&gt; React Native can get you there for simpler, API-driven AI, but be ready to compromise on performance or budget if you push its limits. Don't let framework hype distract you from building actual value.&lt;/p&gt;
&lt;p&gt;Want to talk through your specific AI app idea and figure out the best stack for your budget and timeline? I've been there, done that. Let's schedule a quick call – my calendar link is [Your Calendar Link Here].&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14039/flutter-vs-react-native-ai-apps-my-2026-take</guid>
<pubDate>Mon, 06 Apr 2026 09:04:35 +0000</pubDate>
</item>
<item>
<title>[48 Hours Lost] NativeWind + Expo Router = &quot;Couldn't find a navigation context&quot; Nightmare</title>
<link>https://coderlegion.com/4624/48-hours-lost-nativewind-expo-router-couldnt-find-a-navigation-context-nightmare</link>
<description>&lt;p&gt;I just spent nearly &lt;strong&gt;48 hours debugging&lt;/strong&gt; an error in my Expo Router app that had nothing to do with what the error said.&lt;br&gt;
If you’re using &lt;strong&gt;NativeWind + React Navigation + Expo Router&lt;/strong&gt; — this post is for you. Save your time and sanity.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-error&quot;&gt;The Error&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&quot;Couldn't find a navigation context.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you're here, you’ve probably seen this and thought:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Ah, I must’ve forgotten to wrap something with &lt;code&gt;NavigationContainer&lt;/code&gt;.&quot;&lt;br&gt;
&quot;Maybe the layout is off.&quot;&lt;br&gt;
&quot;Is React Navigation broken?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nope.&lt;/strong&gt; Everything was wired up correctly.&lt;/p&gt;
&lt;p&gt;But navigation still refused to work.&lt;br&gt;
Until I discovered that... &lt;strong&gt;NativeWind&lt;/strong&gt; was silently breaking everything.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-real-culprit-css-interop-race-condition&quot;&gt;The Real Culprit: CSS Interop Race Condition&lt;/h3&gt;
&lt;p&gt;Turns out, &lt;strong&gt;certain NativeWind class names&lt;/strong&gt; trigger runtime CSS parsing at render time.&lt;br&gt;
And if you’re using &lt;strong&gt;Expo Router&lt;/strong&gt;, this creates a subtle race condition:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React Navigation's context hasn't fully initialized yet.&lt;/li&gt;
&lt;li&gt;NativeWind starts processing your &lt;code&gt;className&lt;/code&gt; string.&lt;/li&gt;
&lt;li&gt;React components try to access the navigation context.&lt;/li&gt;
&lt;li&gt;Boom. ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-problematic-nativewind-classes&quot;&gt;⚠️ Problematic NativeWind Classes&lt;/h3&gt;
&lt;p&gt;Here are some of the class names that caused issues in my case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shadow-*&lt;/code&gt; → e.g. &lt;code&gt;shadow-sm&lt;/code&gt;, &lt;code&gt;shadow-md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opacity-*&lt;/code&gt; → e.g. &lt;code&gt;opacity-50&lt;/code&gt;, &lt;code&gt;opacity-70&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bg-color/opacity&lt;/code&gt; → e.g. &lt;code&gt;bg-white/15&lt;/code&gt;, &lt;code&gt;bg-black/30&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text-color/opacity&lt;/code&gt; → e.g. &lt;code&gt;text-white/80&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They work fine in isolation.&lt;br&gt;
But in an &lt;strong&gt;Expo Router layout&lt;/strong&gt;, just one of these can trigger the bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-symptom&quot;&gt;The Symptom&lt;/h3&gt;
&lt;p&gt;If you're hitting any of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;Couldn't find a navigation context&quot;&lt;/code&gt; errors randomly&lt;/li&gt;
&lt;li&gt;App works one minute and crashes the next&lt;/li&gt;
&lt;li&gt;Stack traces point to NavigationContainer setup&lt;/li&gt;
&lt;li&gt;Nothing makes sense anymore&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check your styles.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-fix-use-inline-styles&quot;&gt;✅ The Fix: Use Inline Styles&lt;/h3&gt;
&lt;p&gt;Replace the problematic class names with direct &lt;code&gt;style&lt;/code&gt; props.&lt;/p&gt;
&lt;h4 id=&quot;-before-&quot;&gt;❌ Before:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;TouchableOpacity className=&quot;bg-white/15 shadow-sm opacity-50&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-after-&quot;&gt;✅ After:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;TouchableOpacity 
  style={{
    backgroundColor: 'rgba(255, 255, 255, 0.15)',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.05,
    shadowRadius: 2,
    opacity: 0.5
  }}
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This completely solved the issue in my project.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h3&gt;
&lt;p&gt;This bug showed up in this combo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expo Router v5+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NativeWind v4+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React Navigation v6+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React Native (latest)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;error message is misleading&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;It’s &lt;strong&gt;timing-dependent&lt;/strong&gt; — the app may break or work based on device speed or render cycles&lt;/li&gt;
&lt;li&gt;You waste hours debugging the wrong thing&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-i-did&quot;&gt;✅ What I Did&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Stripped it down to a &lt;strong&gt;minimal reproducible repo&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Posted the issue to &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/marklawlor/nativewind&quot;&gt;NativeWind GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Writing this article to save &lt;strong&gt;you&lt;/strong&gt; from going through it too&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;I love NativeWind.&lt;br&gt;
I love Expo Router.&lt;br&gt;
But the two don’t always play nice — and when they don’t, it can really mess with your head.&lt;/p&gt;
&lt;p&gt;If you hit weird nav context issues in your app:&lt;br&gt;
✅ Check your layout&lt;br&gt;
✅ Then check your NativeWind &lt;code&gt;className&lt;/code&gt;&lt;br&gt;
✅ And maybe… just go inline&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;have-you-seen-this-&quot;&gt;Have You Seen This?&lt;/h3&gt;
&lt;p&gt;If you’ve hit this bug (or others like it), drop a comment.&lt;br&gt;
If this saved you time, please share it — I genuinely hope it helps someone avoid what I went through.&lt;/p&gt;
&lt;p&gt;Thanks for reading. &lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;tags-&quot;&gt;Tags:&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;#reactnative&lt;/code&gt; &lt;code&gt;#expo&lt;/code&gt; &lt;code&gt;#nativewind&lt;/code&gt; &lt;code&gt;#reactnavigation&lt;/code&gt; &lt;code&gt;#debugging&lt;/code&gt; &lt;code&gt;#cssinterop&lt;/code&gt; &lt;code&gt;#mobiledev&lt;/code&gt; &lt;code&gt;#javascript&lt;/code&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4624/48-hours-lost-nativewind-expo-router-couldnt-find-a-navigation-context-nightmare</guid>
<pubDate>Tue, 12 Aug 2025 13:38:49 +0000</pubDate>
</item>
</channel>
</rss>