<?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.us-east-2.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>Your agent looks healthy. It isn’t. Building recursive observability with SigNoz MCP</title>
      <dc:creator>Bhanu Nagpure</dc:creator>
      <pubDate>Sun, 26 Jul 2026 16:06:07 +0000</pubDate>
      <link>https://dev.to/bhanu_nagpure/your-agent-looks-healthy-it-isnt-building-recursive-observability-with-signoz-mcp-4mnh</link>
      <guid>https://dev.to/bhanu_nagpure/your-agent-looks-healthy-it-isnt-building-recursive-observability-with-signoz-mcp-4mnh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hackathon:&lt;/strong&gt; Agents of SigNoz · Track 1 (AI &amp;amp; Agent Observability)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/bhanu-dev82/agentscope" rel="noopener noreferrer"&gt;github.com/bhanu-dev82/agentscope&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Demo video (~2:43):&lt;/strong&gt; &lt;a href="https://github.com/bhanu-dev82/agentscope/blob/master/docs/Demo_AgentScope.mp4" rel="noopener noreferrer"&gt;Demo_AgentScope.mp4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent a solid chunk of this week staring at a dashboard that said everything was fine while the agent underneath was spinning in a tool loop. Latency looked normal. Error rate was zero. HTTP kept returning 200. Tokens kept leaving the account.&lt;/p&gt;

&lt;p&gt;That gap — “green APM, broken agent” — is what pushed me to build &lt;strong&gt;AgentScope&lt;/strong&gt; for the Agents of SigNoz hackathon.&lt;/p&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foscab6p1tyy7keuxtkel.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foscab6p1tyy7keuxtkel.png" alt="AgentScope overview — hero and live stats" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our operator console. The tagline is not marketing fluff; it is the bug report we kept hitting.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why standard APM lies to you about agents
&lt;/h2&gt;

&lt;p&gt;I am not saying traces are useless. They are necessary. But most of the agent failures I care about are &lt;strong&gt;semantic&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same tool fires over and over in one conversation (loop)&lt;/li&gt;
&lt;li&gt;A researcher step burns thousands of tokens on padding or retries&lt;/li&gt;
&lt;li&gt;The model returns confident garbage and the HTTP layer still celebrates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that shows up as a 5xx. Your SLO board stays calm. You only notice when the bill arrives or a user says the answer is nonsense.&lt;/p&gt;

&lt;p&gt;For Track 1 I wanted something sharper than “we exported OpenTelemetry to SigNoz.” The question I wrote on a sticky note was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If an agent can use SigNoz through MCP, can that same agent show up as spans inside SigNoz?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That loop — monitor using the product, then appear inside the product — is what I ended up calling &lt;strong&gt;recursive observability&lt;/strong&gt;. Slightly dramatic name. Accurate description.&lt;/p&gt;


&lt;h2&gt;
  
  
  AgentScope in one picture
&lt;/h2&gt;

&lt;p&gt;Three moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A small multi-agent pipeline (Planner → Researcher → Summarizer) on LangGraph + Gemini
&lt;/li&gt;
&lt;li&gt;Self-hosted &lt;strong&gt;SigNoz&lt;/strong&gt; from Foundry, with MCP turned on
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Watchdog&lt;/strong&gt; process that polls SigNoz MCP, runs detectors, logs interventions, and writes its own spans back&lt;/li&gt;
&lt;/ol&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4rzdgdgv7w9g1xjukj3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4rzdgdgv7w9g1xjukj3.png" alt="Architecture strip and chaos sessions in the UI" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;UI → pipeline → SigNoz → Watchdog. Recent sessions show loop / cost / silent inject modes we actually ran.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Rough data path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser UI (:3000)
    → FastAPI (:8888) runs LangGraph
        → OTLP gRPC (:4317) into SigNoz
            ← Watchdog polls MCP (:8000)
            → Watchdog also exports spans on OTLP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaos buttons in the UI do not flip a boolean for the detector. They make the pipeline emit &lt;strong&gt;real&lt;/strong&gt; span shapes. The Watchdog only sees what ClickHouse has after ingestion. That constraint kept me honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I actually used SigNoz
&lt;/h2&gt;

&lt;p&gt;This is the section judges care about, so I will be concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Foundry, not a mystery compose file
&lt;/h3&gt;

&lt;p&gt;Rules want &lt;code&gt;casting.yaml&lt;/code&gt; and &lt;code&gt;casting.yaml.lock&lt;/code&gt; in the repo. Fair. I cast SigNoz with MCP enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Installation&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signoz&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;flavor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that: UI on 8080, OTLP on 4317, MCP on 8000.&lt;/p&gt;

&lt;p&gt;I lost half an evening when OTLP stopped accepting data while the UI still looked healthy. Turns out OpAMP had rewritten collector pipelines toward &lt;code&gt;nop&lt;/code&gt; receivers. Fix was boring and real: force file-based collector config, recreate the ingester, watch logs until gRPC 4317 came back. If your demo has “no traces,” check the collector before you rewrite application code. I rewrote application code first. Do not be me.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traces with agent-shaped names
&lt;/h3&gt;

&lt;p&gt;I bootstrapped OpenTelemetry &lt;strong&gt;before&lt;/strong&gt; LangChain imports. Order matters; I learned that the hard way when half the spans vanished.&lt;/p&gt;

&lt;p&gt;What landed in SigNoz for service &lt;code&gt;agentscope&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;invoke_workflow AgentPipeline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invoke_agent Planner&lt;/code&gt; / &lt;code&gt;Researcher&lt;/code&gt; / &lt;code&gt;Summarizer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execute_tool web_search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;generate / LLM spans from the model path&lt;/li&gt;
&lt;li&gt;attributes like &lt;code&gt;gen_ai.agent.name&lt;/code&gt;, token usage, conversation id&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not invent a new observability religion here. I just refused to ship a single fat “HTTP POST /run” span and call it agent ops.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP as a polling data plane (not a chat stunt)
&lt;/h3&gt;

&lt;p&gt;A lot of demos open MCP, ask one question, screenshot the answer, done. Fine for a keynote. Useless as an ops loop.&lt;/p&gt;

&lt;p&gt;My Watchdog:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Opens MCP session to &lt;code&gt;http://localhost:8000/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sends &lt;code&gt;SIGNOZ-API-KEY&lt;/code&gt; (created in SigNoz → Settings → API Keys)&lt;/li&gt;
&lt;li&gt;Every ~10 seconds calls tools like &lt;code&gt;signoz_aggregate_traces&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Parses the response&lt;/li&gt;
&lt;li&gt;Runs detectors&lt;/li&gt;
&lt;li&gt;Dedupes on &lt;code&gt;(failure_type, trace_id)&lt;/code&gt; so it does not spam forever&lt;/li&gt;
&lt;li&gt;Writes a critical log (optional webhook if you set one)&lt;/li&gt;
&lt;li&gt;Emits &lt;code&gt;invoke_agent Watchdog&lt;/code&gt; plus child tool spans such as &lt;code&gt;execute_tool signoz_loop_check&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntdxwv361856imp6va27.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntdxwv361856imp6va27.png" alt="Watchdog intervention log after real detections" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Loop, cost spike, silent failure — each with action &lt;code&gt;logged_critical&lt;/code&gt; and a real trace id.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One parser bug cost me more time than the detectors: MCP aggregate payloads are &lt;strong&gt;columnar&lt;/strong&gt; (&lt;code&gt;columns&lt;/code&gt; + &lt;code&gt;data&lt;/code&gt;), not a list of objects. My first code assumed rows-of-dicts, scanned “three rows,” found nothing, and I almost blamed ClickHouse. Always print the raw shape once.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the detectors look for
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Inject&lt;/th&gt;
&lt;th&gt;Pipeline does&lt;/th&gt;
&lt;th&gt;Watchdog looks for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Loop&lt;/td&gt;
&lt;td&gt;Ten &lt;code&gt;web_search&lt;/code&gt; tool spans in one trace&lt;/td&gt;
&lt;td&gt;count of &lt;code&gt;execute_tool *&lt;/code&gt; per (trace, name) &amp;gt; 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost spike&lt;/td&gt;
&lt;td&gt;Heavy token padding on the researcher&lt;/td&gt;
&lt;td&gt;High input token sum on the trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent failure&lt;/td&gt;
&lt;td&gt;Garbage tool output + span &lt;code&gt;semantic_failure silent_output&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Aggregate on name containing &lt;code&gt;semantic_failure&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For loops I group by span &lt;strong&gt;name&lt;/strong&gt; (&lt;code&gt;execute_tool web_search&lt;/code&gt;) more than nested attributes. Attributes were flaky under time pressure; names were stable in the explorer.&lt;/p&gt;

&lt;p&gt;I also had to exclude the Watchdog’s own &lt;code&gt;signoz_*_check&lt;/code&gt; tools from the loop rule. Otherwise the monitor alerts on itself and you invent a very philosophical outage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics, logs, dashboards
&lt;/h3&gt;

&lt;p&gt;Traces carried the story, but I still exported metrics (token usage, durations), bridged Python logs into OTel, and scripted a few dashboards/views through MCP. Not every panel is glamorous. Having &lt;strong&gt;API keys + MCP + Foundry&lt;/strong&gt; in one product is what made the Watchdog feel like a real consumer of SigNoz instead of a side script scraping HTML.&lt;/p&gt;




&lt;h2&gt;
  
  
  The live path I show in the video
&lt;/h2&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdb56zljmfm81ejtu7j9j.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdb56zljmfm81ejtu7j9j.png" alt="Live Demo — chaos inject while HTTP stays green" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Inject loop / cost / silent. Status can still be Complete with a 200 path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hero path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Live Demo, hit &lt;strong&gt;Inject loop&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Pipeline finishes. No HTTP error theater. Error rate still zero.&lt;/li&gt;
&lt;li&gt;Wait. I mean it — &lt;strong&gt;15 to 40 seconds&lt;/strong&gt; for OTLP batch + ClickHouse. The UI even says so. Instant detection would be a lie.&lt;/li&gt;
&lt;li&gt;Watchdog tab lights up: tool &lt;code&gt;web_search&lt;/code&gt; called 10 times, threshold 5, &lt;code&gt;logged_critical&lt;/code&gt;, real &lt;code&gt;trace_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Jump to SigNoz Traces&lt;/li&gt;
&lt;/ol&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmk8zrftkqg3y41cslpf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmk8zrftkqg3y41cslpf.png" alt="SigNoz Traces — invoke_agent Watchdog and signoz_*_check tools" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the recursive proof. Same service name. Monitor spans next to pipeline traffic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;invoke_agent Watchdog&lt;/code&gt; and &lt;code&gt;execute_tool signoz_loop_check&lt;/code&gt; showed up next to the pipeline, the sticky note question was answered. The monitor is not invisible. It is another agent-shaped workload on the same backend.&lt;/p&gt;

&lt;p&gt;Full walkthrough is in the &lt;a href="https://github.com/bhanu-dev82/agentscope/blob/master/docs/Demo_AgentScope.mp4" rel="noopener noreferrer"&gt;demo video&lt;/a&gt; (~2:43). Deck is in the repo if you want slides.&lt;/p&gt;




&lt;h2&gt;
  
  
  Things that bit me (so you skip them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ingestion lag.&lt;/strong&gt; I almost built a fake “instant alert” path for the demo. Glad I did not. Real ClickHouse lag is part of the product story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-alerting.&lt;/strong&gt; Exclude your own MCP tool spans from loop detection. Sounds obvious after midnight. Less obvious before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collector config vs UI health.&lt;/strong&gt; SigNoz UI up ≠ OTLP path healthy. Verify 4317 when traces disappear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attribute worship.&lt;/strong&gt; Nice when they index cleanly. For hackathon detectors, span names got me further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;English + live demo.&lt;/strong&gt; I am not a native English speaker. I wrote a short script, practiced once off camera, and recorded with OBS. Slow is fine. Clear inject → detect → SigNoz is better than fast mumbling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack, short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz&lt;/strong&gt; via Foundry (&lt;code&gt;casting.yaml&lt;/code&gt;, MCP, API keys, traces/metrics/logs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; FastAPI + LangGraph + Gemini&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTel&lt;/strong&gt; traces, metrics, logs to OTLP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt; operator UI for inject + interventions&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/bhanu-dev82/agentscope" rel="noopener noreferrer"&gt;bhanu-dev82/agentscope&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run sketch (details in README):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/bhanu-dev82/agentscope.git
&lt;span class="nb"&gt;cd &lt;/span&gt;agentscope
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# SIGNOZ_API_KEY, GEMINI_API_KEY&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python &lt;span class="nt"&gt;-m&lt;/span&gt; src.run
&lt;span class="nb"&gt;cd &lt;/span&gt;ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm i &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I did not invent agent monitoring. I wired a small multi-agent app to &lt;strong&gt;SigNoz the way the hackathon asks you to&lt;/strong&gt; — Foundry, deep signals, MCP as something an agent actually calls on a timer — and I made the monitor leave fingerprints in the same traces UI.&lt;/p&gt;

&lt;p&gt;If your agents only look healthy, dig one layer under HTTP. And if you build a watchdog, put &lt;em&gt;it&lt;/em&gt; on the board too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recursive observability:&lt;/strong&gt; observe agents with an agent, through SigNoz MCP. The monitor is itself monitored, in the same product.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://github.com/bhanu-dev82/agentscope" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://github.com/bhanu-dev82/agentscope/blob/master/docs/Demo_AgentScope.mp4" rel="noopener noreferrer"&gt;Video&lt;/a&gt; · &lt;a href="https://github.com/bhanu-dev82/agentscope/blob/master/docs/AgentScope_Demo_Deck.pptx" rel="noopener noreferrer"&gt;Deck&lt;/a&gt; · &lt;a href="https://signoz.io" rel="noopener noreferrer"&gt;SigNoz&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Built a Python Learning App From a Hospital Bed. Here’s What I Actually Built.</title>
      <dc:creator>Bhanu Nagpure</dc:creator>
      <pubDate>Tue, 16 Jun 2026 08:03:38 +0000</pubDate>
      <link>https://dev.to/bhanu_nagpure/i-built-a-python-learning-app-from-a-hospital-bed-heres-what-i-actually-built-ac1</link>
      <guid>https://dev.to/bhanu_nagpure/i-built-a-python-learning-app-from-a-hospital-bed-heres-what-i-actually-built-ac1</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%2F31gu2zx81uus786998hc.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%2F31gu2zx81uus786998hc.png" alt="Main Image showing the Home and track selection interface with title I built a Python Learning App!" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I graduated. Then I got sick. Then I built an app.&lt;/p&gt;

&lt;p&gt;I was stuck in a hospital bed, freshly out of college, with nothing but my laptop and a question I couldn't stop thinking about: &lt;em&gt;Why is learning to code still so painful in 2026?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I remembered exactly how it felt to be a beginner staring at a Python tutorial that assumed I cared about abstract theory before writing a single working line. So I built PyMaster. It shipped on January 26, 2026. Here's what's inside - and what it actually took to build it alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Learning That Feels Like&amp;nbsp;Playing
&lt;/h2&gt;

&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%2Fr2ryubhd43ayszbdgbxu.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%2Fr2ryubhd43ayszbdgbxu.png" alt="Image showing the playful UI of PyMaster&amp;nbsp;App." width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The structure is zone-based. You work through &lt;strong&gt;Zone 1: The Foundation &lt;/strong&gt;- &lt;em&gt;First Contact, Memory Bank&lt;/em&gt; (variables), &lt;em&gt;Data Types, The Converter &lt;/em&gt;- unlocking the next zone only when you've genuinely understood the current one. No skipping. No guessing your way through.&lt;/p&gt;

&lt;p&gt;The home screen shows your name, level, streak, coins, and one clear nudge toward what to tackle next. No dashboard overwhelm.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practice Arena: Where the AI&amp;nbsp;Lives
&lt;/h2&gt;

&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%2Fl1qftpxpioiv48p5bdkh.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%2Fl1qftpxpioiv48p5bdkh.png" alt="Image showing the practice screen along with built-in Offline python lab&amp;nbsp;feature" width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
The app watches your performance and identifies weak spots. If your PREDICT_OUTPUT mastery is at 5%, the AI builds a focused session targeting exactly that gap - and tells you so explicitly.&lt;/p&gt;

&lt;p&gt;You pick your format: &lt;strong&gt;Quick&lt;/strong&gt; (5 Qs), &lt;strong&gt;Blitz&lt;/strong&gt; (10 Qs), or &lt;strong&gt;Marathon&lt;/strong&gt; (25 Qs). Then your intensity: Standard play or Sudden Death - 1 life only.&lt;/p&gt;

&lt;p&gt;There's also a &lt;strong&gt;Python Sandbox&lt;/strong&gt; - write and run real Python entirely offline. For a learner with spotty connectivity, this isn't a feature. It's the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI Tutor Runs on&amp;nbsp;Gemini
&lt;/h2&gt;

&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%2F3svgxenzx8fi68fcepew.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%2F3svgxenzx8fi68fcepew.png" alt="Image showing the AI&amp;nbsp;Tutor" width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
The AI tutor is built on &lt;strong&gt;Gemini 3.1 Flash Lite via the Gemini API.&lt;/strong&gt; I chose Flash Lite deliberately - fast, efficient, and perfect for short contextual explanations mid-session.&lt;/p&gt;

&lt;p&gt;Each track has a custom system prompt. The Python Core tutor explains errors like a patient teacher. The GenAI track tutor is configured differently because someone learning to build with generative AI needs different mental models. The mastery detection in Practice Arena feeds directly into what the AI surfaces next. It's woven in, not bolted on.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Antigravity Kept Me From&amp;nbsp;Drowning
&lt;/h2&gt;

&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%2F2lwd8mehq15ko5x3t2pm.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%2F2lwd8mehq15ko5x3t2pm.png" alt="" width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
I built this solo. &lt;strong&gt;Antigravity&lt;/strong&gt; became my agentic development partner in ways I didn't fully anticipate.&lt;/p&gt;

&lt;p&gt;The obvious value was bug detection - catching logic errors in my streak system, flagging edge cases in offline sync I'd have found three weeks later in a Play Store review. But the bigger win was automating the repetitive parts. Every new screen needed the same scaffolding work - new lesson types, new zones, new career tabs. Antigravity automated the template setup. I described what I needed, it generated the structure, I filled in the logic.&lt;/p&gt;

&lt;p&gt;When you're building alone, that's not a productivity gain. That's the difference between shipping and not shipping.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Tracks. One&amp;nbsp;Engine.
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python Core&lt;/strong&gt; - fundamentals, for everyone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Science&lt;/strong&gt; - Pandas, real datasets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenAI&lt;/strong&gt; - building with Generative AI using Gemini&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same engine underneath. Completely different curricula, question banks, and AI tutor behaviour per track.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who's Actually Using&amp;nbsp;It
&lt;/h2&gt;

&lt;p&gt;I built this with Indian students in mind. The reality turned out bigger.&lt;/p&gt;

&lt;p&gt;Users are coming in from &lt;strong&gt;South Asia, South Africa, and Brazil&lt;/strong&gt; - spending &lt;strong&gt;15 to 30 minutes per session&lt;/strong&gt;, which for mobile learning is a genuinely strong engagement number. That's not a passive scroll. That's someone working through a lesson.&lt;/p&gt;

&lt;p&gt;The geographic spread confirmed something I suspected: this isn't an Indian problem. It's a global one, concentrated wherever laptop-first education has left learners behind.&lt;/p&gt;

&lt;p&gt;Content updates are coming. The question banks will grow, new zones will ship, and the AI tutor will get sharper. The foundation is in place.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Tell Someone Starting&amp;nbsp;Out
&lt;/h2&gt;

&lt;p&gt;Build something real. Use Gemini not just as a coding assistant but as a thought partner - ask it &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;what&lt;/em&gt;. And use agentic tools to kill the mechanical work so your energy goes into making the product actually good.&lt;/p&gt;

&lt;p&gt;PyMaster is live on the &lt;a href="https://play.google.com/store/apps/details?id=com.devanshustudios.pymaster" rel="noopener noreferrer"&gt;Google Play Store&lt;/a&gt;. Built by one developer, from a hospital bed, shipped on Republic Day.&lt;/p&gt;

&lt;p&gt;I'd love to hear what you think.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>flutter</category>
      <category>pymaster</category>
      <category>python</category>
    </item>
    <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>
