<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bhanu Nagpure</title>
    <description>The latest articles on DEV Community by Bhanu Nagpure (@bhanu_nagpure).</description>
    <link>https://dev.to/bhanu_nagpure</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3820364%2Fb0cdcd7f-912f-46da-a63c-f5f30bdf6bca.jpg</url>
      <title>DEV Community: Bhanu Nagpure</title>
      <link>https://dev.to/bhanu_nagpure</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bhanu_nagpure"/>
    <language>en</language>
    <item>
      <title>I Didn't Stress Test My Native Ads. My App Paid the Price.</title>
      <dc:creator>Bhanu Nagpure</dc:creator>
      <pubDate>Wed, 01 Apr 2026 11:30:59 +0000</pubDate>
      <link>https://dev.to/bhanu_nagpure/i-didnt-stress-test-my-native-ads-my-app-paid-the-price-2gd9</link>
      <guid>https://dev.to/bhanu_nagpure/i-didnt-stress-test-my-native-ads-my-app-paid-the-price-2gd9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkkktky1i4sqhkis6hegl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkkktky1i4sqhkis6hegl.png" alt="Example of AdMob native ads with 0.5 percent match rate causing low impressions" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I launched &lt;strong&gt;PyMaster&lt;/strong&gt; — a gamified Python learning app and everything looked fine — until my AdMob match rate dropped to 0.5%.&lt;br&gt;
Here’s the mistake that caused it (and how to avoid it).&lt;/p&gt;

&lt;p&gt;A few months in, I shipped an update I was proud of. Within days, my AdMob dashboard was showing something I didn't expect: &lt;strong&gt;~2,000 requests. ~10 impressions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That wasn't a bad ad day. That was a self-inflicted wound.&lt;/p&gt;

&lt;p&gt;Here's what I got wrong, what it cost me, and the smarter path I found on the other side.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Setup: Native Ads in a Scrollable List
&lt;/h2&gt;

&lt;p&gt;My home screen is a vertical list — chapters, zones, locked content. Classic stuff.&lt;/p&gt;

&lt;p&gt;I slotted a native ad tile into the list. It worked beautifully in testing. Loaded, rendered, sat quietly between content.&lt;/p&gt;

&lt;p&gt;So I got ambitious. I added logic to &lt;strong&gt;retry loading&lt;/strong&gt; if an ad failed — because why waste a slot? If AdMob couldn't fill it, at least try again, right?&lt;/p&gt;

&lt;p&gt;The implementation looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// native_ad_tile.dart (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NativeAdTile&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeAdTile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_NativeAdTileState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_NativeAdTileState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeAdTile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;NativeAd&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_nativeAd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isLoaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_adFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;didChangeDependencies&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;didChangeDependencies&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Retry logic: reload if not yet failed&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_nativeAd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_adFailed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_loadAd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_loadAd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_nativeAd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NativeAd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;adUnitId:&lt;/span&gt; &lt;span class="s"&gt;'your-ad-unit-id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;listener:&lt;/span&gt; &lt;span class="n"&gt;NativeAdListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;onAdLoaded:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_isLoaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;onAdFailedToLoad:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_adFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ← This was missing initially&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_adFailed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shrink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_isLoaded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_buildSkeleton&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AdWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ad:&lt;/span&gt; &lt;span class="n"&gt;_nativeAd&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Defensive. I was proud of it.&lt;/p&gt;

&lt;p&gt;Then I shipped the update and forgot to ask one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when AdMob has zero ads to serve?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero-fill regions broke my ad logic.
&lt;/h2&gt;

&lt;p&gt;AdMob fill rates are not uniform across regions. In some markets — including places like Burkina Faso — native ad inventory can be extremely limited or even unavailable. In those cases, AdMob will accept the request but return a no-fill error.&lt;/p&gt;

&lt;p&gt;My retry logic didn’t account for that. It assumed an ad would eventually load — so it kept trying.&lt;/p&gt;

&lt;p&gt;Every scroll. Every &lt;code&gt;didChangeDependencies&lt;/code&gt; call. Every widget rebuild.&lt;/p&gt;

&lt;p&gt;The list had multiple ad slots. Each one repeatedly requesting ads.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;~2,000 ad requests. ~10 impressions.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s a &lt;strong&gt;0.5% fill rate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This kind of request-to-impression imbalance can negatively impact performance signals like match rate and eCPM over time. When your app generates many requests but very few impressions, it may indicate inefficient ad loading or poor integration patterns.&lt;/p&gt;

&lt;p&gt;The effect compounds quietly in the background — while everything looks fine on the surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lesson: Think in Request/Impression Ratio
&lt;/h2&gt;

&lt;p&gt;When you're building ad integrations, &lt;strong&gt;the metric that matters most isn't revenue per impression&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;request/impression ratio&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A ratio far above 1.0 is a red flag. It tells AdMob's systems that your integration is spammy, unreliable, or poorly targeted. This can reduce the chances of receiving high-value ads. Your eCPM drops. Your fill rate drops further. It compounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The golden rule for ad requests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make one request per ad slot per session&lt;/li&gt;
&lt;li&gt;If it fails → mark it as failed, collapse the space, and move on&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;not&lt;/strong&gt; retry indefinitely on failure&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;not&lt;/strong&gt; reload on every widget lifecycle event
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ The defensive pattern&lt;/span&gt;
&lt;span class="nl"&gt;onAdFailedToLoad:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_adFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Mark permanently failed&lt;/span&gt;
      &lt;span class="n"&gt;_isLoaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// No retry. No loop. Just fail gracefully.&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Collapse space on failure — don't show an empty box&lt;/span&gt;
&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_adFailed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shrink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Zero height. Zero cost.&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Best Practices for Native Ads in Flutter (The Hard-Won List)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Always use test IDs in debug mode&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kDebugMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;adUnitId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAndroid&lt;/span&gt;
    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'ca-app-pub-3940256099942544/2247696110'&lt;/span&gt;
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'ca-app-pub-3940256099942544/3986624511'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never test with production IDs. Never.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Gate production loading strictly&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// If the production ID is missing, do nothing.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adUnitId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;adUnitId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_adFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty ad is better than a test ad in production. An empty ad is better than a crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Use &lt;code&gt;AutomaticKeepAliveClientMixin&lt;/code&gt; to prevent reload storms&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a scrollable list, widgets get disposed and recreated as you scroll. Without keep-alive, every scroll past an ad tile triggers a new ad request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_NativeAdTileState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeAdTile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;AutomaticKeepAliveClientMixin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;wantKeepAlive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Required&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Load the ad once — whether you use &lt;code&gt;initState&lt;/code&gt; or &lt;code&gt;didChangeDependencies&lt;/code&gt;, just make sure it’s properly guarded so you don’t trigger multiple requests.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;didChangeDependencies&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;didChangeDependencies&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_nativeAd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_adFailed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_loadAd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// The guard prevents double-loading&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Always dispose&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;_nativeAd&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miss this once and you'll have memory leaks and phantom requests running in the background.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Pulled the Plug. And It Felt Good.
&lt;/h2&gt;

&lt;p&gt;After diagnosing the damage, I made the call: &lt;strong&gt;remove native ads entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Within 24 hours of shipping that update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request count dropped to near zero&lt;/li&gt;
&lt;li&gt;The relief was immediate and visible in the dashboard&lt;/li&gt;
&lt;li&gt;My match rate started recovering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Native ads in a scrollable list with unknown regional fill rates were doing more harm than good.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Replaced Them With
&lt;/h2&gt;

&lt;p&gt;Here's the clever bit.&lt;/p&gt;

&lt;p&gt;Instead of an ad tile, I now have an &lt;strong&gt;in-house promotional tile&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Same slot. Same height. Same visual weight in the list.&lt;/p&gt;

&lt;p&gt;But it promotes &lt;strong&gt;PyMaster Pro&lt;/strong&gt; — my own upgrade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No AdMob. No fill rate. No requests. Just yours.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProUpgradeTile&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="n"&gt;BoxDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;gradient:&lt;/span&gt; &lt;span class="n"&gt;LinearGradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;colors:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orange&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="nl"&gt;borderRadius:&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;star&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;white&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Go Pro — Unlock All Chapters"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;TextButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_openPaywall&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Upgrade"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The slot that was burning my request ratio and damaging rankings is now a direct conversion surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;100% fill rate. 0 AdMob requests. You own the real estate.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I Did Wrong&lt;/th&gt;
&lt;th&gt;What to Do Instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retried ad load on every rebuild&lt;/td&gt;
&lt;td&gt;Mark failed, collapse, move on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No regional fill-rate awareness&lt;/td&gt;
&lt;td&gt;Assume zero fill is possible anywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native ads in a high-scroll list&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;AutomaticKeepAliveClientMixin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measured success by impressions&lt;/td&gt;
&lt;td&gt;Measure request/impression ratio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kept native ads when they hurt&lt;/td&gt;
&lt;td&gt;Know when to cut your losses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Stress testing ad integrations isn't just about "does the ad show."&lt;/p&gt;

&lt;p&gt;It's about how your app behaves when it doesn’t.&lt;/p&gt;

&lt;p&gt;Simulate real failure conditions — poor connectivity, no-fill responses — and watch what your UI actually does.&lt;/p&gt;

&lt;p&gt;The bugs that hurt aren’t the visible ones. They’re the silent loops in the background, draining performance while everything looks fine on the surface.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code defensively. Watch your ratios.&lt;br&gt;
And design for failure — not just success.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;I’m Bhanu, solo founder at Devanshu Studio, building PyMaster — a gamified Python learning app for mobile.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;If you're learning Python on mobile, it's built for you → &lt;a href="https://play.google.com/store/apps/details?id=com.devanshustudios.pymaster" rel="noopener noreferrer"&gt;Check it out on the Play Store&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;flutter&lt;/code&gt; &lt;code&gt;admob&lt;/code&gt; &lt;code&gt;mobile&lt;/code&gt; &lt;code&gt;indie-dev&lt;/code&gt; &lt;code&gt;showdev&lt;/code&gt; &lt;code&gt;android&lt;/code&gt; &lt;code&gt;monetization&lt;/code&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why I Stopped Building Apps and Started Building an Engine in Flutter</title>
      <dc:creator>Bhanu Nagpure</dc:creator>
      <pubDate>Thu, 12 Mar 2026 12:46:49 +0000</pubDate>
      <link>https://dev.to/bhanu_nagpure/why-i-stopped-building-apps-and-started-building-an-engine-in-flutter-406</link>
      <guid>https://dev.to/bhanu_nagpure/why-i-stopped-building-apps-and-started-building-an-engine-in-flutter-406</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37ob7ps5yqvt7jrf1wbo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37ob7ps5yqvt7jrf1wbo.png" alt="Architecture diagram" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an indie developer, your biggest bottleneck isn’t skill — it’s time. And maintaining multiple apps will absolutely destroy it.&lt;/p&gt;

&lt;p&gt;A few months ago, I started building &lt;strong&gt;&lt;a href="https://play.google.com/store/apps/details%20id=com.devanshustudios.pymaster" rel="noopener noreferrer"&gt;PyMaster&lt;/a&gt;&lt;/strong&gt;, a gamified app to help people learn Python on their phones. But the moment I shipped the first version, I didn’t feel done. I wanted to teach SQL next. Then JavaScript. Maybe even Rust.&lt;/p&gt;

&lt;p&gt;That’s when I realized I had a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Copy-Paste Trap
&lt;/h2&gt;

&lt;p&gt;The obvious move was to duplicate the PyMaster codebase, swap the logos, change the content, and publish a new app.&lt;/p&gt;

&lt;p&gt;Simple, right?&lt;/p&gt;

&lt;p&gt;I actually tried it. I cloned the repo, renamed a few things, and within ten minutes I already hated what I was looking at.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three separate codebases
&lt;/li&gt;
&lt;li&gt;Three separate sets of bugs
&lt;/li&gt;
&lt;li&gt;Three separate times I’d have to push a fix every time I touched the streak logic
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a solo founder, that’s not a roadmap — that’s a slow death.&lt;/p&gt;

&lt;p&gt;So I scrapped it and spent a weekend thinking differently.&lt;/p&gt;

&lt;p&gt;Instead of building another app, I’d build an &lt;strong&gt;engine&lt;/strong&gt; — one codebase that could compile into an infinite number of apps.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;white-label educational platform&lt;/strong&gt;, powered by &lt;strong&gt;Flutter + Riverpod&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here’s exactly how I architected it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Concept: Inversion of Control
&lt;/h2&gt;

&lt;p&gt;In a normal app, your UI knows too much.&lt;/p&gt;

&lt;p&gt;It’s got hardcoded strings like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome to Python"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also has tightly coupled parsers that only understand Python syntax.&lt;/p&gt;

&lt;p&gt;The UI is opinionated, and that opinion is baked in deep.&lt;/p&gt;

&lt;p&gt;To white-label this, I needed the UI to become &lt;strong&gt;completely dumb&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It shouldn’t know what it’s teaching — only &lt;strong&gt;how to render what it’s given&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I achieved this by creating a master interface called &lt;code&gt;CourseBlueprint&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The master contract every app flavor must fulfill&lt;/span&gt;
&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CourseBlueprint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;appTitle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;virtualCurrencyName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Theme Engine&lt;/span&gt;
  &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;brandPrimaryColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;brandSecondaryColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Logic Injection&lt;/span&gt;
  &lt;span class="n"&gt;CodeParserStrategy&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;codeParser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Per-app 3rd Party Config&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;analyticsId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;aiSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This interface is the &lt;strong&gt;backbone of the entire system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every flavor of the app — Python, SQL, JavaScript — must implement it.&lt;/p&gt;

&lt;p&gt;The UI never imports a &lt;code&gt;PythonCodeParser&lt;/code&gt; directly.&lt;br&gt;&lt;br&gt;
It just asks for a parser, and the configuration provides one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating the “Flavors”
&lt;/h2&gt;

&lt;p&gt;With the blueprint in place, spinning up a new app takes &lt;strong&gt;hours, not weeks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what the &lt;strong&gt;Python flavor&lt;/strong&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Python-specific implementation&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PythonCourseConfig&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;CourseBlueprint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;appTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"PyMaster"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;virtualCurrencyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Tokens"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;brandPrimaryColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFFFD43B&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Python Yellow&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;CodeParserStrategy&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;codeParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PythonCodeParser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Handles indentation&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;aiSystemPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;"You are an expert Python tutor. Explain the error in simple terms."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I want to launch a &lt;strong&gt;SQL app tomorrow&lt;/strong&gt;, I simply:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;SqlCourseConfig&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Change the theme color&lt;/li&gt;
&lt;li&gt;Inject a &lt;code&gt;SqlKeywordParser&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Update the AI prompt&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s genuinely it.&lt;/p&gt;

&lt;p&gt;The real win here isn’t technical elegance.&lt;/p&gt;

&lt;p&gt;It’s where my &lt;strong&gt;time goes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of wrestling infrastructure, I spend almost all my effort on the &lt;strong&gt;content inside the app&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tying It Together with a Factory
&lt;/h2&gt;

&lt;p&gt;How does the app know which configuration to load?&lt;/p&gt;

&lt;p&gt;I use Flutter’s &lt;code&gt;--dart-define&lt;/code&gt; flag during compilation.&lt;/p&gt;

&lt;p&gt;A simple factory reads the flavor and returns the right config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlueprintFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;CourseBlueprint&lt;/span&gt; &lt;span class="n"&gt;getForFlavor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;flavor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flavor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;'python'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PythonCourseConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;'sql'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SqlCourseConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PythonCourseConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Safe fallback&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ll be honest.&lt;/p&gt;

&lt;p&gt;The first time I wired this up, I spent nearly &lt;strong&gt;two hours debugging&lt;/strong&gt; why my SQL flavor kept rendering in Python Yellow.&lt;/p&gt;

&lt;p&gt;Turns out I forgot to pass the build flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--dart-define&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;APP_FLAVOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it silently fell back to the default.&lt;/p&gt;

&lt;p&gt;Infuriating in the moment.&lt;br&gt;&lt;br&gt;
Obvious in hindsight. Classic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Injecting Config into the UI with Riverpod
&lt;/h2&gt;

&lt;p&gt;The last piece was making the configuration &lt;strong&gt;globally accessible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Without prop-drilling it through every widget.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Riverpod&lt;/strong&gt; shines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Global provider — overridden at startup&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;blueprintProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CourseBlueprint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;UnimplementedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Must be overridden in main.dart"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;flavor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'APP_FLAVOR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;defaultValue:&lt;/span&gt; &lt;span class="s"&gt;'python'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;selectedConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BlueprintFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getForFlavor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flavor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ProviderScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;overrides:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;blueprintProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;overrideWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectedConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyEduApp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any widget can read the configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WidgetRef&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blueprintProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Welcome to &lt;/span&gt;&lt;span class="si"&gt;${config.appTitle}&lt;/span&gt;&lt;span class="s"&gt;!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;brandPrimaryColor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI is completely &lt;strong&gt;blind to the domain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It simply renders what it’s given.&lt;/p&gt;

&lt;p&gt;That’s the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Changes Everything for a Solo Founder
&lt;/h2&gt;

&lt;p&gt;Every bug I fix in the &lt;strong&gt;core gamification engine&lt;/strong&gt; — streaks, XP, offline progression — gets fixed for &lt;strong&gt;every app&lt;/strong&gt; compiled from this codebase.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write the fix once&lt;/li&gt;
&lt;li&gt;Ship the improvement once&lt;/li&gt;
&lt;li&gt;Every flavor inherits it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That leverage is everything for a solo developer.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;I’m betting this architecture will outlive any single app I build with it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Whether that’s a smart long-term bet or just what you tell yourself after spending a weekend over-engineering…&lt;/p&gt;

&lt;p&gt;I guess we’ll find out.&lt;/p&gt;




&lt;p&gt;If you want to see how this engine feels in production, I just shipped the first flavor:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.devanshustudios.pymaster" rel="noopener noreferrer"&gt;PyMaster&lt;/a&gt;&lt;/strong&gt; is now live on the Google Play Store, featuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gamified offline progression&lt;/li&gt;
&lt;li&gt;a dynamic theme engine&lt;/li&gt;
&lt;li&gt;a built-in AI tutor&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;Have you architected something similar?&lt;/p&gt;

&lt;p&gt;Did you go with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;app flavors
&lt;/li&gt;
&lt;li&gt;monorepos
&lt;/li&gt;
&lt;li&gt;plugin architectures
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m curious what trade-offs you ran into.&lt;/p&gt;

&lt;p&gt;Drop your thoughts in the comments 👇&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
