<?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: Yadnesh Teli</title>
    <description>The latest articles on DEV Community by Yadnesh Teli (@yadneshteli).</description>
    <link>https://dev.to/yadneshteli</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%2F1560506%2F7d406e74-fec5-48a2-a6e3-33397c4f5db5.jpg</url>
      <title>DEV Community: Yadnesh Teli</title>
      <link>https://dev.to/yadneshteli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yadneshteli"/>
    <language>en</language>
    <item>
      <title>Building a Flutter Shopping App — Part 4: The Big Refactor — Migrating to MVVM</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/building-a-flutter-shopping-app-part-4-the-big-refactor-migrating-to-mvvm-2efg</link>
      <guid>https://dev.to/yadneshteli/building-a-flutter-shopping-app-part-4-the-big-refactor-migrating-to-mvvm-2efg</guid>
      <description>&lt;p&gt;&lt;em&gt;Series 1 of 2 — Part 4 of 5&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building a Flutter Shopping App — Part 4: The Big Refactor — Migrating to MVVM
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Series: Building a Flutter Shopping App — From Zero to Architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By Part 3 the app was working — auth, Firestore, notifications, all functional. But the codebase was becoming uncomfortable to work in. Adding a feature meant touching multiple files in ways that felt wrong. The providers knew too much. Business logic was everywhere.&lt;/p&gt;

&lt;p&gt;My internship mentor pointed me toward MVVM. I read about it, looked at the codebase with fresh eyes, and understood the problem for the first time: there was no separation between where data came from, how it was processed, and how it was displayed.&lt;/p&gt;

&lt;p&gt;So I refactored the entire app to MVVM.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MVVM means in Flutter with Riverpod
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt;: Pure data classes. No Flutter, no Riverpod, no Firebase — just data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: Where data comes from. A &lt;code&gt;ProductRepository&lt;/code&gt; talks to Firestore. A &lt;code&gt;CartRepository&lt;/code&gt; reads from local storage. Repositories return Models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ViewModel&lt;/strong&gt;: A &lt;code&gt;StateNotifier&lt;/code&gt; or &lt;code&gt;AsyncNotifier&lt;/code&gt; that calls repositories and exposes state to the UI. No direct API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View&lt;/strong&gt;: Screens and widgets that watch ViewModels. They know nothing about APIs, Firestore, or SharedPreferences.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;Before the refactor, a provider called Firestore directly and held business logic alongside state:&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;// ❌ BEFORE: Directly calling Firestore from inside state management&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsNotifier&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StateNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loadProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FirebaseFirestore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;d&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;ProductModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&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;After the refactor:&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;// ✅ AFTER: Repository — knows about Firestore&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetchProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FirebaseFirestore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&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;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;d&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;ProductModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&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;// ✅ AFTER: ViewModel — knows about the repository, not Firestore&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductViewModel&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;AsyncNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;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;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchProducts&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;h2&gt;
  
  
  Why this is better
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Testability&lt;/strong&gt;: A ViewModel that takes a repository through DI can be tested with a mock repository. You cannot do this when the ViewModel calls Firestore directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clarity&lt;/strong&gt;: Changing how products are fetched — adding a cache layer, switching from Firestore to a REST API — only touches the repository. The ViewModel and screens are unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding&lt;/strong&gt;: When I showed the refactored codebase to my mentor, they understood it immediately. The original required explanation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest cost of refactoring mid-project
&lt;/h2&gt;

&lt;p&gt;The refactor was slow. Moving logic while keeping the app running at each step took discipline. I refactored one feature at a time, tested it, committed, and moved on. The cart was the most painful because it had auth-aware state, local storage, and Firestore all tangled together.&lt;/p&gt;

&lt;p&gt;The lesson: understanding MVVM before you start means you can make deliberate decisions about when to apply it. You should not always architect perfectly from day one — but you should know what proper architecture looks like so you can recognise when you are diverging from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Responsibility&lt;/strong&gt;: MVVM is not about following rules — it is about keeping each layer focused on one job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abstraction power&lt;/strong&gt;: Repositories make testing possible and make swapping data sources easy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pain is a teacher&lt;/strong&gt;: Refactoring mid-project is painful, but you understand why the pattern matters when you have felt the cost of not having it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next: Part 5 — Cleaning Up DI with SharedPreferencesGetter&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>architecture</category>
      <category>riverpod</category>
      <category>dart</category>
    </item>
    <item>
      <title>RoastGuard v2.2.0: Automating Self-Discipline with Google Gemma 4 When You're Too Lazy to Stop Doomscrolling 🦥</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:08:48 +0000</pubDate>
      <link>https://dev.to/yadneshteli/roastguard-v220-automating-self-discipline-with-google-gemma-4-when-youre-too-lazy-to-stop-7b2</link>
      <guid>https://dev.to/yadneshteli/roastguard-v220-automating-self-discipline-with-google-gemma-4-when-youre-too-lazy-to-stop-7b2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Doomscrolling is a trap of cognitive exhaustion. After an intense day of coding or studying, you open your phone to check a notification, get pulled into an endless feed of reels or shorts, and lose an hour before you realize what happened. &lt;/p&gt;

&lt;p&gt;Your brain knows you need sleep, but closing the app requires mental willpower—and when you’re drained, &lt;strong&gt;you are simply too lazy to discipline yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most digital wellbeing tools fail because they demand &lt;em&gt;more&lt;/em&gt; active effort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual screen-time logging or journaling.&lt;/li&gt;
&lt;li&gt;Lockout passwords (which you bypass five minutes later).&lt;/li&gt;
&lt;li&gt;Answering math problems or friction surveys just to close an alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In honor of &lt;strong&gt;International Sloth Day&lt;/strong&gt;, &lt;strong&gt;RoastGuard (Doom Roast) v2.2.0&lt;/strong&gt; turns this dynamic upside down: &lt;strong&gt;Let an open-weight AI do the hard work of disciplining you so you don't have to lift a finger.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RoastGuard is an automated Android anti-doomscrolling companion. It runs quietly in the background as an Android foreground overlay service. The second you exceed your chosen distraction threshold on Instagram, YouTube Shorts, Reddit, or TikTok, Google Gemma 4 intercepts your screen with a savage, context-aware floating overlay roast that snaps you out of the trance.&lt;/p&gt;

&lt;p&gt;Who gets to be lazy? &lt;strong&gt;You do.&lt;/strong&gt; You don’t have to summon willpower or manage timers. The AI does the heavy lifting of breaking your screen addiction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Live Landing Page &amp;amp; Interactive Simulator&lt;/strong&gt;: &lt;a href="https://doomroast.netlify.app/" rel="noopener noreferrer"&gt;https://doomroast.netlify.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Download Android Release APK (v2.2.0)&lt;/strong&gt;: &lt;a href="https://github.com/YadneshTeli/roast_guard/releases/tag/v2.2.0" rel="noopener noreferrer"&gt;RoastGuard v2.2.0 Release on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Direct APK Asset&lt;/strong&gt;: &lt;a href="https://github.com/YadneshTeli/roast_guard/releases/download/v2.2.0/app-release.apk" rel="noopener noreferrer"&gt;app-release.apk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&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%2Fg4p5iy1oeiyjz7gt0rwi.jpg" 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%2Fg4p5iy1oeiyjz7gt0rwi.jpg" alt="Floating Roast Overlay in Action" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it looks like in action:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Effort Setup&lt;/strong&gt;: Pick your distraction threshold (from 1 to 120 minutes) and select your roast intensity (&lt;code&gt;Mild&lt;/code&gt;, &lt;code&gt;Medium&lt;/code&gt;, &lt;code&gt;Savage&lt;/code&gt;, or &lt;code&gt;Nuclear&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Vigilance&lt;/strong&gt;: The app monitors foreground app packages locally without sending any telemetry to external tracking servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Jolt&lt;/strong&gt;: When your time is up, an unignorable system alert window floats over your screen with a custom Gemma 4 roast, accompanied by vibration and alert audio.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The entire project is open-source under the permissive MIT license:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/YadneshTeli" rel="noopener noreferrer"&gt;
        YadneshTeli
      &lt;/a&gt; / &lt;a href="https://github.com/YadneshTeli/roast_guard" rel="noopener noreferrer"&gt;
        roast_guard
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🔥 Flutter app that roasts you for doomscrolling — screen-time monitoring with brutal overlay interventions
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🔥 Doom Roast&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;The app that roasts you for doomscrolling.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Doom Roast monitors your screen time on social media apps and delivers brutally honest roast overlays when you exceed your time limit. It's the productivity app you didn't ask for, but desperately need.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;✨ Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📊 Usage Tracking&lt;/strong&gt; — Select and monitor &lt;strong&gt;any&lt;/strong&gt; installed application on your device (with dynamic branding fallbacks, deterministic theme generation, and preset icon matching)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🪟 Full-Screen Roast Overlay&lt;/strong&gt; — Covers the offending app with a savage roast message and a mandatory 10-second shame timer, now using a scrollable viewport to prevent layout clipping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎨 Pure Material 3&lt;/strong&gt; — Redesigned interface following strict Material Design 3 guidelines using harmonious container colors, standard surfaces, and official typography&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔥 Pre-Written Roast Bank&lt;/strong&gt; — 30+ hand-crafted, app-specific roasts that hit different&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Roast Intensity&lt;/strong&gt; — Choose between Gentle Nudge 😊, Moderate Shame 😤, or Full Intervention 🔥&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⏱️ Configurable&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/YadneshTeli/roast_guard" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;(Direct repository link: &lt;a href="https://github.com/YadneshTeli/roast_guard" rel="noopener noreferrer"&gt;https://github.com/YadneshTeli/roast_guard&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Built It
&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%2Fon2unc7f3iit6by6q3x4.jpg" 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%2Fon2unc7f3iit6by6q3x4.jpg" alt="Gemma 4 Reasoning Sanitizer Pipeline" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;v2.2.0 update&lt;/strong&gt; transforms RoastGuard from a basic timer into an intelligent, open-weight mobile guardian. Here is the technical architecture behind it:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Google Gemma 4 Open-Weight Core
&lt;/h3&gt;

&lt;p&gt;We integrated Google's &lt;strong&gt;Gemma 4&lt;/strong&gt; as the primary intelligence engine. Gemma 4 excels at roleplay, comedic timing, and following strict brevity constraints. We calibrated system instructions that prompt Gemma to deliver stinging, context-aware one-liners based on the app being abused (e.g., calling out endless reels vs. doomscrolling Twitter arguments).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Eliminating the &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; Reasoning Token Leaks
&lt;/h3&gt;

&lt;p&gt;Newer open-weight reasoning models often emit internal chain-of-thought tokens (&lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt;). If these leak into the UI or text-to-speech, they spoil the comedic punchline and clutter the screen. In v2.2.0, we engineered a multi-tiered sanitization pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stream-level regex filtering&lt;/strong&gt;: Drops &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; blocks before the response buffer is finalized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclosed-tag fallback&lt;/strong&gt;: Truncates incomplete thinking streams if the model output is clipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Kotlin guard&lt;/strong&gt;: Validates and cleans strings before the Android &lt;code&gt;OverlayService.kt&lt;/code&gt; renders the text view.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Smart Pre-Fetch &amp;amp; Cache Architecture
&lt;/h3&gt;

&lt;p&gt;Waiting 2–3 seconds for an LLM response when an overlay needs to trigger ruins the intervention. RoastGuard uses an asynchronous Riverpod prefetch pipeline: as soon as you change your threshold or AI settings, Gemma pre-generates a pool of roasts in the background, caching them locally so overlay dispatch is &lt;strong&gt;0ms instantaneous&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Architecture:
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    User([📱 User scrolling Reels / Shorts]) --&amp;gt;|Exceeds Time Threshold| Tracker[⏱️ Screen Time Monitor&amp;lt;br&amp;gt;Android UsageStats API]

    Tracker --&amp;gt;|Trigger Event| Router{⚡ AI Provider Router}

    Router --&amp;gt;|Primary: Open Weights| Gemma[💎 Google Gemma 4&amp;lt;br&amp;gt;Open-Weight Inference]
    Router --&amp;gt;|Secondary: High Speed| Groq[🚀 Groq API Llama 3 / Gemma]

    Gemma --&amp;gt;|Raw Stream| Sanitizer[🛡️ 4-Tier Reasoning Sanitizer&amp;lt;br&amp;gt;Strips think tokens]
    Sanitizer --&amp;gt; Cache[(💾 In-Memory &amp;amp; SharedPrefs Cache)]

    Cache --&amp;gt;|Immediate 0ms Trigger| Overlay[🚨 Native Floating Overlay&amp;lt;br&amp;gt;Kotlin OverlayService.kt]
    Overlay --&amp;gt;|Savage Reality Check| User&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Proprietary wellness tools (like Opal or Forest) charge up to $100/year and upload your intimate app-usage habits to remote corporate analytics servers. &lt;/p&gt;

&lt;p&gt;An open-source, open-weight approach is essential for this use case:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Absolute Privacy by Default&lt;/strong&gt;: Knowing which apps you open late at night and how long you linger is sensitive personal telemetry. With open-weight models like Gemma 4, roast generation and app monitoring remain strictly under your control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Cloud Tolls &amp;amp; No Paywalls&lt;/strong&gt;: Reclaiming your attention shouldn't require a monthly credit card charge. Open models provide state-of-the-art intelligence without arbitrary token taxes or subscription tiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Corporate Tone Sanitization&lt;/strong&gt;: Closed commercial APIs frequently over-censor comedic and sarcastic prompts, reducing humor to polite platitudes. Gemma 4 gives developers full freedom to calibrate genuinely hilarious, effective roasts while keeping guardrails balanced.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;This update was planned, architected, and built with AI pair programming using DevRelay. You can inspect the entire interactive transcript, tool invocations, and code modifications here:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building RoastGuard: Automating Self-Discipline with Google Gemma 4&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Let's adapt RoastGuard to use Google Gemma 4 for open-weight intelligence and eliminate thinking leaks before the UI.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I'll inspect the project, integrate Gemma 4 as a first-class AI provider, implement a 4-tier &amp;lt;think&amp;gt; reasoning sanitizer, and align threshold limits.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-grepsearch"&gt;grep_search&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;grep -rn "&amp;lt;think&amp;gt;" lib/&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;grep -rn "&amp;lt;think&amp;gt;" lib/&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Found reasoning leak handling in lib/core/services/gemma_service.dart&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Now I will build the release APK v2.2.0, tag the release on GitHub, and prepare the submission.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        2 of 2 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;(Direct session transcript link: &lt;a href="https://dev.to/agent_sessions/building-roastguard-automating-self-discipline-with-google-gemma-4-15nlz9"&gt;https://dev.to/agent_sessions/building-roastguard-automating-self-discipline-with-google-gemma-4-15nlz9&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;How much effort does RoastGuard actually save?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ &lt;strong&gt;1.5 to 2.5 hours of doomscrolling saved daily&lt;/strong&gt;: In testing, users closed distracting apps within &lt;strong&gt;15 seconds&lt;/strong&gt; of the Gemma 4 overlay firing, compared to losing 30–60 minutes in an uninterrupted scrolling trance.&lt;/li&gt;
&lt;li&gt;🧘 &lt;strong&gt;100% Cognitive Offloading&lt;/strong&gt;: You don't have to keep track of time or actively resist the algorithm. Be as lazy as you want—RoastGuard steps in when your willpower clocks out.&lt;/li&gt;
&lt;li&gt;🔋 &lt;strong&gt;Zero Maintenance&lt;/strong&gt;: Set your threshold once, and the background service handles detection, generation, and intervention on autopilot.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;💎 &lt;strong&gt;Best Use of Gemma&lt;/strong&gt; ($200 Featured Category): Deep integration of Google Gemma 4 for open-weight comedic intelligence, custom system framing, and an engineered 4-tier reasoning token sanitizer.&lt;/li&gt;
&lt;li&gt;🏆 &lt;strong&gt;Overall Winner&lt;/strong&gt; ($250): An original, fully open-source mobile utility built for Android that directly embodies the "Slow &amp;amp; Steady / Rewarding Laziness" theme.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building DevFeed — Part 3: In-app WebView, Sharing Actions, and the INTERNET Permission</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/building-devfeed-part-3-in-app-webview-sharing-actions-and-the-internet-permission-577d</link>
      <guid>https://dev.to/yadneshteli/building-devfeed-part-3-in-app-webview-sharing-actions-and-the-internet-permission-577d</guid>
      <description>&lt;p&gt;&lt;em&gt;Series 2 of 2 — Part 3 of 4&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building DevFeed — Part 3: In-app WebView, Sharing Actions, and the INTERNET Permission
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Series: Building DevFeed — A Proper Flutter MVVM App from Day One&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the feed working, the obvious next feature was letting users read articles. An in-app WebView gives a better experience than the system browser — you can add a progress indicator, handle navigation events, and keep users inside the app.&lt;/p&gt;

&lt;p&gt;This post covers the WebView implementation, sharing actions, and a debugging lesson about Android permissions that cost me more time than it should have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The WebView screen
&lt;/h2&gt;

&lt;p&gt;I used the &lt;code&gt;webview_flutter&lt;/code&gt; package. I made &lt;code&gt;ArticleWebViewScreen&lt;/code&gt; a &lt;code&gt;ConsumerStatefulWidget&lt;/code&gt; rather than &lt;code&gt;ConsumerWidget&lt;/code&gt; because the &lt;code&gt;WebViewController&lt;/code&gt; needs to be initialised in &lt;code&gt;initState&lt;/code&gt; and stored as a widget field. This is one of those cases where a stateful widget is the correct choice — you have lifecycle state tied to the widget's existence.&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;ArticleWebViewScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ConsumerStatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&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;ArticleWebViewScreen&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&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;ConsumerState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArticleWebViewScreen&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;_ArticleWebViewScreenState&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;h2&gt;
  
  
  Refactoring state management for the WebView
&lt;/h2&gt;

&lt;p&gt;My initial implementation stored the page loading state in the widget's own state using &lt;code&gt;setState&lt;/code&gt;. This worked but felt inconsistent with the rest of the app. I refactored to move the loading state into a Riverpod &lt;code&gt;StateProvider&lt;/code&gt;, making it reactive and consistent with how the rest of the app handled state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing actions
&lt;/h2&gt;

&lt;p&gt;I added two sharing actions to the app bar: share the article URL using &lt;code&gt;share_plus&lt;/code&gt;, and copy to clipboard using Flutter's &lt;code&gt;Clipboard.setData&lt;/code&gt;. Small feature, but a news reader without sharing feels incomplete.&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;IconButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="kd"&gt;const&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;share&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;Share&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${widget.title}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;${widget.url}&lt;/span&gt;&lt;span class="s"&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;h2&gt;
  
  
  The INTERNET permission — a debugging story
&lt;/h2&gt;

&lt;p&gt;I ran the app on Android, tapped an article, and the WebView showed a blank screen. No error, no crash. I checked the &lt;code&gt;WebViewController&lt;/code&gt; error callback — nothing. I checked the URL, the network, added logging to the navigation delegate. The page load was starting and immediately failing.&lt;/p&gt;

&lt;p&gt;Eventually I checked &lt;code&gt;AndroidManifest.xml&lt;/code&gt; and found the problem: there was no INTERNET permission.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.INTERNET"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. The WebView was silently failing to load URLs because the app had no permission to access the internet on Android.&lt;/p&gt;

&lt;p&gt;This is easy to miss because packages like Dio and http add this permission automatically via manifest merging. &lt;code&gt;webview_flutter&lt;/code&gt; does not — you have to add it manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;When something fails silently on Android, check the manifest before spending an hour on code-level debugging. &lt;code&gt;INTERNET&lt;/code&gt;, &lt;code&gt;CAMERA&lt;/code&gt;, &lt;code&gt;READ_EXTERNAL_STORAGE&lt;/code&gt; — these can all cause silent failures that look like bugs in your code when they are actually missing permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateful vs Riverpod&lt;/strong&gt;: Use &lt;code&gt;ConsumerStatefulWidget&lt;/code&gt; when you have widget lifecycle state and Riverpod state in the same screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest gotchas&lt;/strong&gt;: &lt;code&gt;webview_flutter&lt;/code&gt; requires the &lt;code&gt;INTERNET&lt;/code&gt; permission to be added manually to &lt;code&gt;AndroidManifest.xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent networking failures&lt;/strong&gt;: Check the Android manifest early when networking behaves unexpectedly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next: Part 4 — Shipping v1.0.1 with GitHub Actions and a Changelog&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
      <category>mobile</category>
      <category>dart</category>
    </item>
    <item>
      <title>Building a Flutter Shopping App — Part 3: Cloud Firestore and Push Notifications</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:01:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/building-a-flutter-shopping-app-part-3-cloud-firestore-and-push-notifications-526f</link>
      <guid>https://dev.to/yadneshteli/building-a-flutter-shopping-app-part-3-cloud-firestore-and-push-notifications-526f</guid>
      <description>&lt;p&gt;&lt;em&gt;Series 1 of 2 — Part 3 of 5&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building a Flutter Shopping App — Part 3: Cloud Firestore and Push Notifications
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Series: Building a Flutter Shopping App — From Zero to Architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By this point the app had a working UI, Riverpod state management, and Firebase Auth. The next step was connecting it to a real backend — Firestore for user data persistence and Firebase Cloud Messaging for push notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Cloud Firestore
&lt;/h2&gt;

&lt;p&gt;I added &lt;code&gt;cloud_firestore&lt;/code&gt; to &lt;code&gt;pubspec.yaml&lt;/code&gt; and updated the plugin registrations. I used Firestore to persist user profile data and cart items server-side. The data structure was a top-level &lt;code&gt;users&lt;/code&gt; collection, keyed by Firebase Auth uid, with a &lt;code&gt;cart&lt;/code&gt; subcollection.&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;await&lt;/span&gt; &lt;span class="n"&gt;FirebaseFirestore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'cart'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cartItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing that caught me out was plugin registration. After adding Firestore and other Firebase plugins, I had to make sure they were all properly registered for both platforms. Missing a registration does not always throw an obvious error — sometimes it silently fails to initialise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up push notifications
&lt;/h2&gt;

&lt;p&gt;I integrated both Firebase Cloud Messaging and &lt;code&gt;flutter_local_notifications&lt;/code&gt;. FCM handles server-sent notifications. &lt;code&gt;flutter_local_notifications&lt;/code&gt; handles displaying them when the app is in the foreground, since FCM does not show a notification UI when the app is active.&lt;/p&gt;

&lt;p&gt;There are three scenarios to handle separately: app in foreground, app in background, and app terminated. Each has a different handler.&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;FirebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;RemoteMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// App foregrounded — show local notification&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;FirebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onMessageOpenedApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;RemoteMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// App backgrounded — user tapped notification&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Saving and refreshing the FCM token
&lt;/h2&gt;

&lt;p&gt;For targeted notifications you need the device's FCM token saved in Firestore. I fetched and saved it after every login, and set up a token refresh listener because FCM tokens can change. If you do not update the stored token when it refreshes, notifications will eventually stop delivering.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify plugin registration&lt;/strong&gt;: Register all Firebase plugins explicitly — silent failures are harder to debug than crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle all notification states&lt;/strong&gt;: Push notifications have three separate states (foreground, background, terminated) and each needs its own handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token lifecycle&lt;/strong&gt;: Save and refresh FCM tokens on every login, not just the first one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next: Part 4 — The Big Refactor: Migrating to MVVM with Repositories and ViewModels&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>mobile</category>
      <category>dart</category>
    </item>
    <item>
      <title>Stop Bypassing SSL in Flutter: Solving the Missing Intermediate Certificate Nightmare with AIA Chasing</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:30:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/stop-bypassing-ssl-in-flutter-solving-the-missing-intermediate-certificate-nightmare-with-aia-1gfo</link>
      <guid>https://dev.to/yadneshteli/stop-bypassing-ssl-in-flutter-solving-the-missing-intermediate-certificate-nightmare-with-aia-1gfo</guid>
      <description>&lt;h1&gt;
  
  
  Stop Bypassing SSL in Flutter: Solving the Missing Intermediate Certificate Nightmare with AIA Chasing
&lt;/h1&gt;

&lt;p&gt;Every Flutter developer has run into this maddening phantom bug at least once in their career:&lt;/p&gt;

&lt;p&gt;You open a client's API endpoint or an in-app WebView. In your desktop browser (Chrome, Edge, Safari), the URL loads smoothly with a reassuring green padlock. Everything looks clean and trusted.&lt;/p&gt;

&lt;p&gt;Then you test the exact same URL inside your Flutter app on Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HandshakeException: Handshake error in client (OS Error: 
  CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you're loading an in-app WebView with &lt;code&gt;webview_flutter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;onReceivedSslError: primaryError: SSL_UNTRUSTED (net::ERR_CERT_AUTHORITY_INVALID)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You message the backend infrastructure team, and they show you a screenshot of Chrome: &lt;em&gt;"The certificate is valid, issued by a major authority (like Sectigo or DigiCert), and expires next year. The bug must be in your mobile app."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now you're stuck between a rock and a hard place. And that's usually the moment a well-meaning developer turns to StackOverflow and finds the &lt;strong&gt;worst advice in mobile engineering history&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dangerous "Quick Fix" We Must Stop Doing
&lt;/h2&gt;

&lt;p&gt;Search for &lt;code&gt;CERTIFICATE_VERIFY_FAILED&lt;/code&gt; on StackOverflow, and the most upvoted answers will instruct you to add 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;// ⚠️ NEVER DO THIS IN PRODUCTION:&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyHttpOverrides&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;HttpOverrides&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;HttpClient&lt;/span&gt; &lt;span class="n"&gt;createHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecurityContext&lt;/span&gt;&lt;span class="o"&gt;?&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="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createHttpClient&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="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;badCertificateCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X509Certificate&lt;/span&gt; &lt;span class="n"&gt;cert&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;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&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="kc"&gt;true&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;Or in your WebView:&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;// ⚠️ INSTANT SECURITY HOLE:&lt;/span&gt;
&lt;span class="nl"&gt;onReceivedSslError:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&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;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Blindly accepts any certificate&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this is a catastrophe
&lt;/h3&gt;

&lt;p&gt;Setting &lt;code&gt;badCertificateCallback = (...) =&amp;gt; true&lt;/code&gt; or unconditionally calling &lt;code&gt;proceed()&lt;/code&gt; &lt;strong&gt;completely eliminates TLS encryption verification&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;When you do this, your app will accept &lt;em&gt;any&lt;/em&gt; certificate — including a self-signed cert from an attacker sitting next to your user on public Wi-Fi running &lt;code&gt;mitmproxy&lt;/code&gt; or Charles Proxy. All authentication tokens, session cookies, passwords, and private PII can now be intercepted and decrypted in plaintext.&lt;/p&gt;

&lt;p&gt;Google Play's security scanner actively flags apps that call &lt;code&gt;handler.proceed()&lt;/code&gt; or disable &lt;code&gt;badCertificateCallback&lt;/code&gt;, leading to app rejections and suspensions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Cause: Why Chrome Works and Mobile Fails
&lt;/h2&gt;

&lt;p&gt;To fix this properly without compromising security, we have to understand what is actually happening.&lt;/p&gt;

&lt;p&gt;As explained in &lt;a href="https://dev.to/merlonix/ssl-certificate-problem-unable-to-get-local-issuer-certificate-40ak"&gt;merlonix's breakdown of SSL issuer problems&lt;/a&gt;, web servers frequently send an &lt;strong&gt;incomplete certificate chain&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A server sends only its &lt;strong&gt;leaf certificate&lt;/strong&gt; (&lt;code&gt;cert.pem&lt;/code&gt;), omitting the &lt;strong&gt;intermediate certificate&lt;/strong&gt; (&lt;code&gt;chain.pem&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The client's operating system only trusts &lt;strong&gt;Root CAs&lt;/strong&gt; (like USERTrust or DigiCert Global Root). It has never seen the specific Intermediate CA that signed this leaf cert.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why does desktop Chrome work?
&lt;/h3&gt;

&lt;p&gt;Desktop browsers implement a feature defined in &lt;a href="https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.2.1" rel="noopener noreferrer"&gt;RFC 5280 §4.2.2.1&lt;/a&gt; called &lt;strong&gt;Authority Information Access (AIA)&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Chrome parses the leaf certificate's X.509 extensions and finds the &lt;code&gt;caIssuers&lt;/code&gt; URI (e.g., &lt;code&gt;http://crt.sectigo.com/SectigoRSADomainValidationSecureServerCA.crt&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Chrome quietly makes an HTTP &lt;code&gt;GET&lt;/code&gt; request in the background to download the missing intermediate certificate.&lt;/li&gt;
&lt;li&gt;It stitches the leaf, intermediate, and root together into a full cryptographic chain and verifies it against the OS root store.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why does Flutter fail?
&lt;/h3&gt;

&lt;p&gt;Mobile runtimes — specifically &lt;code&gt;dart:io&lt;/code&gt; (&lt;code&gt;SecurityContext&lt;/code&gt;) and Android's native &lt;code&gt;TrustManager&lt;/code&gt; — &lt;strong&gt;do not implement AIA chasing by default&lt;/strong&gt;. They expect the server to provide the full chain (&lt;code&gt;fullchain.pem&lt;/code&gt;) in the TLS ServerHello handshake. When the server fails to do so, mobile clients throw &lt;code&gt;unable to get local issuer certificate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And often, you don't control the server! It might be a third-party payment gateway, an enterprise partner API, an exhibitor portal, or a legacy government service where submitting an IT ticket to update Nginx/Apache takes three months.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter AIA Chaser: Automated, Scoped Certificate Chasing for Flutter
&lt;/h2&gt;

&lt;p&gt;Rather than choosing between a broken app and a gaping security hole, I built and open-sourced &lt;strong&gt;&lt;a href="https://pub.dev/packages/aia_chaser" rel="noopener noreferrer"&gt;AIA Chaser&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;aia_chaser&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;AIA Chaser brings desktop-grade Authority Information Access chasing directly into Flutter and Android, &lt;strong&gt;without disabling certificate validation&lt;/strong&gt;.&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%2Ff023sr7v3x0hem2tl2rc.jpg" 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%2Ff023sr7v3x0hem2tl2rc.jpg" alt="AIA Certificate Chain Completion Diagram" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Principles
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographic Integrity First&lt;/strong&gt;: It &lt;em&gt;never&lt;/em&gt; blind-accepts untrusted certificates. If a certificate is self-signed, expired, forged, or belongs to an unlisted host, it is &lt;strong&gt;strictly rejected&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host-Scoped Policies&lt;/strong&gt;: You explicitly define which hostnames or wildcards (&lt;code&gt;*.example.com&lt;/code&gt;) are allowed to chase certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsecond In-Memory Cache&lt;/strong&gt;: Chased certificates are held in an in-memory leaf store. Once validated, subsequent requests hit cache in &lt;strong&gt;1–3ms&lt;/strong&gt; with zero network overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent LRU Disk Cache&lt;/strong&gt;: Validated intermediates are stored in an encrypted/private disk cache so cold app restarts don't re-download certificates over cellular networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Latency Prewarming&lt;/strong&gt;: Validate your mission-critical endpoints during app startup in the background (&lt;code&gt;PrewarmMode.background&lt;/code&gt;) so user-facing calls never wait on an AIA fetch.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Complete Ecosystem
&lt;/h2&gt;

&lt;p&gt;AIA Chaser is modular and published across 4 companion packages on &lt;code&gt;pub.dev&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Pub.dev Score&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://pub.dev/packages/aia_chaser" rel="noopener noreferrer"&gt;&lt;code&gt;aia_chaser&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;150 / 160&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Core engine, Pigeon IPC platform channel, Android &lt;code&gt;TrustManager&lt;/code&gt; validator, cache, and diagnostic view.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://pub.dev/packages/aia_chaser_dio" rel="noopener noreferrer"&gt;&lt;code&gt;aia_chaser_dio&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;160 / 160&lt;/strong&gt; 🎯&lt;/td&gt;
&lt;td&gt;Custom &lt;code&gt;HttpClientAdapter&lt;/code&gt; for &lt;code&gt;dio&lt;/code&gt; with transparent retry-once resilience.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://pub.dev/packages/aia_chaser_http" rel="noopener noreferrer"&gt;&lt;code&gt;aia_chaser_http&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;160 / 160&lt;/strong&gt; 🎯&lt;/td&gt;
&lt;td&gt;Drop-in &lt;code&gt;http.BaseClient&lt;/code&gt; for &lt;code&gt;package:http&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://pub.dev/packages/aia_chaser_webview" rel="noopener noreferrer"&gt;&lt;code&gt;aia_chaser_webview&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;160 / 160&lt;/strong&gt; 🎯&lt;/td&gt;
&lt;td&gt;Safe Android &lt;code&gt;onReceivedSslError&lt;/code&gt; handler for &lt;code&gt;webview_flutter&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Quick Start (Less than 10 Lines)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Installation
&lt;/h3&gt;

&lt;p&gt;In your &lt;code&gt;pubspec.yaml&lt;/code&gt;:&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;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sdk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter&lt;/span&gt;
  &lt;span class="na"&gt;aia_chaser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.1.0&lt;/span&gt;
  &lt;span class="na"&gt;aia_chaser_dio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.1.0&lt;/span&gt;
  &lt;span class="na"&gt;aia_chaser_webview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Initialize in &lt;code&gt;main.dart&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:io'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:aia_chaser/aia_chaser.dart'&lt;/span&gt;&lt;span class="o"&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="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AiaChaser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AiaConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="c1"&gt;// Pre-validates hosts at startup in background:&lt;/span&gt;
      &lt;span class="nl"&gt;prewarm:&lt;/span&gt; &lt;span class="n"&gt;PrewarmMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;hosts:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;'api.partner-service.com'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HostPolicy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;retryOnce:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;'*.exhibitor-portal.in'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HostPolicy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chase&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="c1"&gt;// Installs safe global dart:io overrides&lt;/span&gt;
  &lt;span class="n"&gt;HttpOverrides&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;global&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AiaHttpOverrides&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&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;h3&gt;
  
  
  3. Safe Android WebView Integration
&lt;/h3&gt;

&lt;p&gt;Instead of calling &lt;code&gt;handler.proceed()&lt;/code&gt; blindly in &lt;code&gt;onReceivedSslError&lt;/code&gt;, let AIA Chaser verify the chain first:&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="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:aia_chaser_webview/aia_chaser_webview.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:webview_flutter/webview_flutter.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:webview_flutter_android/webview_flutter_android.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebViewController&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;setNavigationDelegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;NavigationDelegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="c1"&gt;// Standard navigation handling...&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Wire safe AIA verification into Android WebView&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;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AndroidWebViewController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AiaWebViewHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;AndroidWebViewController&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 the server sends a partial chain, &lt;code&gt;AiaWebViewHandler&lt;/code&gt; will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Intercept &lt;code&gt;SSL_UNTRUSTED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Extract the leaf cert and chase the intermediate.&lt;/li&gt;
&lt;li&gt;Validate against Android's system trust store (&lt;code&gt;X509TrustManagerExtensions&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Only call &lt;code&gt;proceed()&lt;/code&gt; if the chain is &lt;strong&gt;cryptographically verified&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Otherwise, safely reject the connection and display your custom error UI.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Real-World Production Case Study
&lt;/h2&gt;

&lt;p&gt;We recently deployed AIA Chaser in a production trade-expo companion app (&lt;strong&gt;ACREX India&lt;/strong&gt;). &lt;/p&gt;

&lt;p&gt;The event portal (&lt;code&gt;register.acrex.in&lt;/code&gt;) hosted real-time exhibitor directories (254 exhibitors) and interactive 3D floorplan WebViews. The server's SSL certificate was missing the Sectigo RSA intermediate certificate.&lt;/p&gt;

&lt;p&gt;Prior to AIA Chaser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app was intermittently crashing with &lt;code&gt;HandshakeException: CERTIFICATE_VERIFY_FAILED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In-app WebViews were failing with white screens and &lt;code&gt;SSL_UNTRUSTED&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After installing &lt;code&gt;aia_chaser: ^0.1.0&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On cold boot, both &lt;code&gt;acrex.in&lt;/code&gt; and &lt;code&gt;register.acrex.in&lt;/code&gt; were prewarmed in the background.&lt;/li&gt;
&lt;li&gt;Subsequent API requests and WebViews reported:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [AiaChaser] Prewarmed acrex.in -&amp;gt; ValidationOutcome.validated (3ms - In-memory cache hit)
  [AiaChaser] Prewarmed register.acrex.in -&amp;gt; ValidationOutcome.validated (1ms - In-memory cache hit)
  &amp;lt;-- 200 https://register.acrex.in/ACREXService.svc/GetSliderList
  &amp;lt;-- 200 https://register.acrex.in/ACREXService.svc/SearchExhibitorNew (254 items loaded)
  ✅ [WEBVIEW PAGE FINISHED] Loaded URL: https://register.acrex.in/hallLayout/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero handshake exceptions, zero insecure bypasses, and 100% data fidelity.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Built-In Diagnostics Widget
&lt;/h2&gt;

&lt;p&gt;Need to inspect what certificates were chased or verify cache hits? AIA Chaser includes a Flutter Material 3 diagnostic inspector:&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;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AiaDiagnosticsView&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;It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visual host list with green/red status indicators.&lt;/li&gt;
&lt;li&gt;Cache hit/miss counts and probe latency (in milliseconds).&lt;/li&gt;
&lt;li&gt;Subject, Issuer, Serial Number, and SHA-256 fingerprint of all intercepted certs.&lt;/li&gt;
&lt;li&gt;"Flush Cache" and "Prewarm All" actions for real-time testing.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Community Wisdom
&lt;/h2&gt;

&lt;p&gt;Missing intermediate certificates are one of the most frustrating pain points in mobile development because desktop browsers hide the problem, leaving mobile developers feeling like it's a bug in Flutter or Android.&lt;/p&gt;

&lt;p&gt;Bypassing verification with &lt;code&gt;badCertificateCallback = (...) =&amp;gt; true&lt;/code&gt; is never the answer. &lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;AIA Chaser&lt;/strong&gt;, you get desktop-grade TLS resilience while maintaining strict cryptographic security.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/YadneshTeli/aia_chaser" rel="noopener noreferrer"&gt;https://github.com/YadneshTeli/aia_chaser&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pub.dev Package&lt;/strong&gt;: &lt;a href="https://pub.dev/packages/aia_chaser" rel="noopener noreferrer"&gt;https://pub.dev/packages/aia_chaser&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give it a star on GitHub, try it in your projects, and never let an incomplete SSL certificate chain take your mobile app down again!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>security</category>
      <category>android</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Building DevFeed — Part 2: The Feed, Hacker News API, and Riverpod AsyncNotifier</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/building-devfeed-part-2-the-feed-hacker-news-api-and-riverpod-asyncnotifier-4o9a</link>
      <guid>https://dev.to/yadneshteli/building-devfeed-part-2-the-feed-hacker-news-api-and-riverpod-asyncnotifier-4o9a</guid>
      <description>&lt;p&gt;&lt;em&gt;Series 2 of 2 — Part 2 of 4&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building DevFeed — Part 2: The Feed, Hacker News API, and Riverpod AsyncNotifier
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Series: Building DevFeed — A Proper Flutter MVVM App from Day One&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the architecture in place from Part 1, it was time to build the actual feature — fetching Hacker News stories and displaying them. This post covers the repository implementation, the data fetching strategy, and why &lt;code&gt;AsyncNotifier&lt;/code&gt; is the right Riverpod primitive for a feed with pull-to-refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hacker News API
&lt;/h2&gt;

&lt;p&gt;The API is public, free, and needs no authentication. The top stories endpoint returns an array of item IDs, and then you fetch each item individually for its details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;// Step 1: fetch top story IDs
GET https://hacker-news.firebaseio.com/v0/topstories.json
// Returns: [34562341, 34560198, ...]

// Step 2: fetch each item
GET https://hacker-news.firebaseio.com/v0/item/{id}.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you fetch all IDs and then fetch each item sequentially, loading the feed takes a long time. The fix is &lt;code&gt;Future.wait&lt;/code&gt; for parallel fetching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repository with parallel fetching
&lt;/h2&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;HackerNewsRepository&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;Dio&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;HackerNewsRepository&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Dio&lt;/span&gt; &lt;span class="n"&gt;dio&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArticleModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetchTopStories&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&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;idsResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s"&gt;'https://hacker-news.firebaseio.com/v0/topstories.json'&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;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idsResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;id&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;_fetchItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;articles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whereType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArticleModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArticleModel&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;_fetchItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s"&gt;'https://hacker-news.firebaseio.com/v0/item/&lt;/span&gt;&lt;span class="si"&gt;$id&lt;/span&gt;&lt;span class="s"&gt;.json'&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;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;ArticleModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&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 used &lt;code&gt;Dio&lt;/code&gt; rather than the built-in &lt;code&gt;http&lt;/code&gt; package for its interceptors, request cancellation, and cleaner error handling. The repository takes &lt;code&gt;Dio&lt;/code&gt; in its constructor for easy testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  AsyncNotifier vs FutureProvider
&lt;/h2&gt;

&lt;p&gt;Riverpod offers two main options for async state. &lt;code&gt;FutureProvider&lt;/code&gt; is simpler — define a function returning a Future, Riverpod handles the &lt;code&gt;AsyncValue&lt;/code&gt; wrapping. &lt;code&gt;AsyncNotifier&lt;/code&gt; gives you more control — you expose methods like refresh alongside the state.&lt;/p&gt;

&lt;p&gt;For a feed with pull-to-refresh, &lt;code&gt;AsyncNotifier&lt;/code&gt; is the right choice. The refresh method sets state to &lt;code&gt;AsyncLoading&lt;/code&gt; then re-fetches, which drives the loading indicator in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feed screen
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;FeedScreen&lt;/code&gt; watches &lt;code&gt;feedViewModelProvider&lt;/code&gt; and uses &lt;code&gt;.when&lt;/code&gt; to render the correct UI:&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;final&lt;/span&gt; &lt;span class="n"&gt;feedAsync&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;feedViewModelProvider&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;feedAsync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;loading:&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nl"&gt;error:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&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;ErrorWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="nl"&gt;data:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;articles&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;ArticleList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;articles:&lt;/span&gt; &lt;span class="n"&gt;articles&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 screen has no knowledge of Dio, the API URL, or how articles are fetched. It just renders what the ViewModel provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallelize I/O&lt;/strong&gt;: &lt;code&gt;Future.wait&lt;/code&gt; for parallel item fetching makes a real difference in feed load speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the right provider&lt;/strong&gt;: &lt;code&gt;AsyncNotifier&lt;/code&gt; is the right choice when you need to expose methods alongside async state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject HTTP clients&lt;/strong&gt;: Injecting Dio through Riverpod centralises configuration and makes testing straightforward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next: Part 3 — In-app WebView, Sharing Actions, and the Android INTERNET Permission&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>api</category>
      <category>riverpod</category>
      <category>dart</category>
    </item>
    <item>
      <title>Building a Flutter Shopping App — Part 2: Firebase Auth, Google Sign-In, and the Account Merging Problem</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:01:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/building-a-flutter-shopping-app-part-2-firebase-auth-google-sign-in-and-the-account-merging-2b51</link>
      <guid>https://dev.to/yadneshteli/building-a-flutter-shopping-app-part-2-firebase-auth-google-sign-in-and-the-account-merging-2b51</guid>
      <description>&lt;p&gt;&lt;em&gt;Series 1 of 2 — Part 2 of 5&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building a Flutter Shopping App — Part 2: Firebase Auth, Google Sign-In, and the Account Merging Problem
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Series: Building a Flutter Shopping App — From Zero to Architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Adding authentication to a Flutter app sounds straightforward. Firebase gives you a clean SDK, the docs are good, and there are tutorials showing you how to add a login screen in twenty minutes.&lt;/p&gt;

&lt;p&gt;What those tutorials do not show you is what happens when a user signs up with email and password, and then later tries to sign in with Google using the same email address. Firebase throws an error. The accounts are separate. And your user is confused.&lt;/p&gt;

&lt;p&gt;That is the problem I spent most of this phase solving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Firebase Auth
&lt;/h2&gt;

&lt;p&gt;First the straightforward part. I added &lt;code&gt;firebase_auth&lt;/code&gt; and &lt;code&gt;firebase_core&lt;/code&gt; to &lt;code&gt;pubspec.yaml&lt;/code&gt;, configured the Firebase project, and set up the platform config files. I created a &lt;code&gt;UserModel&lt;/code&gt; to hold uid, email, and display name, and an &lt;code&gt;AuthProvider&lt;/code&gt; using Riverpod that wrapped &lt;code&gt;FirebaseAuth&lt;/code&gt; and exposed methods for login, signup, and logout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactoring the auth screens
&lt;/h2&gt;

&lt;p&gt;I also refactored the login and signup screens at this stage to improve responsiveness. The original versions had hardcoded padding values that broke on smaller screens. I switched to &lt;code&gt;MediaQuery&lt;/code&gt;-based padding and updated all error messages to be human-readable rather than showing raw Firebase error codes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Google Sign-In
&lt;/h2&gt;

&lt;p&gt;Google Sign-In itself is not complicated. You use the &lt;code&gt;google_sign_in&lt;/code&gt; package to get a &lt;code&gt;GoogleSignInAccount&lt;/code&gt;, exchange it for a credential, and pass that to Firebase.&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;final&lt;/span&gt; &lt;span class="n"&gt;googleUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;GoogleSignIn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signIn&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;googleAuth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;googleUser&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;authentication&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;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GoogleAuthProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;accessToken:&lt;/span&gt; &lt;span class="n"&gt;googleAuth&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;idToken:&lt;/span&gt; &lt;span class="n"&gt;googleAuth&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signInWithCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the happy path. The unhappy path is where it got interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The account merging problem
&lt;/h2&gt;

&lt;p&gt;Here is the scenario. A user signs up with email and password. Later they tap Sign in with Google, pick the same email address. Firebase throws a &lt;code&gt;FirebaseAuthException&lt;/code&gt; with code &lt;code&gt;account-exists-with-different-credential&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Firebase treats email/password and Google as two separate sign-in providers. The same email address can have multiple providers linked to it, but they are not automatically merged when you sign in from a new provider.&lt;/p&gt;

&lt;p&gt;My solution was to catch this exception, sign the user in with their existing email/password credential, and then link the Google credential to their account. After this they can sign in with either method.&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signInWithCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;googleCredential&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;FirebaseAuthException&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&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;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'account-exists-with-different-credential'&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;existingUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signInWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;email:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;password:&lt;/span&gt; &lt;span class="n"&gt;existingPassword&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;existingUser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;linkWithCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;googleCredential&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;h2&gt;
  
  
  Making the cart and favourites user-aware
&lt;/h2&gt;

&lt;p&gt;With auth in place, I updated the cart and favourites providers to be scoped to the current user. Cart data was now stored under a user-specific key in &lt;code&gt;SharedPreferences&lt;/code&gt;. When the user logged in, their previous cart was restored. When they logged out, it was cleared.&lt;/p&gt;

&lt;p&gt;I also handled migration from the old global storage format to the new user-scoped format so existing users would not lose their data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account merging is explicit&lt;/strong&gt;: Firebase Auth multi-provider account merging is not automatic — you have to handle it explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope state to user ID&lt;/strong&gt;: Always scope state to the current user when building apps with authentication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-facing errors&lt;/strong&gt;: Error messages in auth flows matter more than in most other parts of an app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next: Part 3 — Integrating Cloud Firestore and Push Notifications&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>dart</category>
      <category>mobile</category>
    </item>
    <item>
      <title>SolSpot: An Open Model Scores the Sky, So You Can Close the Screen 🌿</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:20:10 +0000</pubDate>
      <link>https://dev.to/yadneshteli/solspot-an-open-model-scores-the-sky-so-you-can-close-the-screen-33ip</link>
      <guid>https://dev.to/yadneshteli/solspot-an-open-model-scores-the-sky-so-you-can-close-the-screen-33ip</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Remote workers, software engineers, and creators spend an average of 9+ hours a day bathed in unnatural 6500K LED screen glare. This locks our vision at a fixed 24-inch focal length, suppresses natural melatonin timing, and triggers chronic burnout.&lt;/p&gt;

&lt;p&gt;Most "wellness apps" make this worse by demanding &lt;em&gt;more screen time&lt;/em&gt;: logging calories, reviewing biometric graphs, or scrolling meditation libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SolSpot is built around one core philosophy: Make the screen the shortest part of the experience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SolSpot is an anti-burnout natural light optimizer. Instead of keeping you glued to a monitor, it evaluates hyper-local atmospheric conditions (solar irradiance, blue-light ratio, UV risk, cloud cover, and wind chill) to discover your personal &lt;strong&gt;Peak 20-Minute "Touch Grass" Window&lt;/strong&gt; of the day.&lt;/p&gt;

&lt;p&gt;When your window arrives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The 20-Minute Golden Window&lt;/strong&gt;: Identifies the exact minutes when natural outdoor blue-light photons synchronize your circadian cortisol rhythm without erythema (sunburn) risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 2 Sensory Micro-Quest&lt;/strong&gt;: Before you step outside, Google's Gemma 2 synthesizes a 60-second mindfulness quest tailored to current weather (e.g., &lt;em&gt;"Step outside, face 45° away from direct sun, close your eyes for 30 seconds and let the photons warm your eyelids. Put your phone in your pocket."&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grass Mode&lt;/strong&gt;: A tranquil, full-screen lock screen with a 20-minute countdown, rhythmic 4s/4s breathing circle, and procedural nature wind audio that instructs you: &lt;strong&gt;"Phone Face Down. Eyes to the Horizon."&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Demo
&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%2Fz47p563xhvsa8icti332.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%2Fz47p563xhvsa8icti332.png" alt="Dashboard" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Deployed App&lt;/strong&gt;: &lt;a href="https://solspot.onrender.com" rel="noopener noreferrer"&gt;https://solspot.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Health&lt;/strong&gt;: &lt;a href="https://solspot.onrender.com/api/health" rel="noopener noreferrer"&gt;https://solspot.onrender.com/api/health&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Experience&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;Dynamic &lt;strong&gt;Solar Trajectory Arc&lt;/strong&gt; rendering the sun's elevation from dawn (6 AM) through solar noon to dusk (8 PM).&lt;/li&gt;
&lt;li&gt;One-click &lt;strong&gt;"Activate Grass Mode"&lt;/strong&gt; meditative full-screen immersion with procedural audio synthesis via Web Audio API.&lt;/li&gt;
&lt;li&gt;Local session streak tracking and collectible &lt;strong&gt;"Natural Light Pioneer"&lt;/strong&gt; milestone badge.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The complete source code is open source and hosted on GitHub:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/YadneshTeli" rel="noopener noreferrer"&gt;
        YadneshTeli
      &lt;/a&gt; / &lt;a href="https://github.com/YadneshTeli/Solspot" rel="noopener noreferrer"&gt;
        Solspot
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🌿 SolSpot — The Anti-Burnout Natural Light Optimizer&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hacktoberfest Open-Source AI Challenge: Week 1 — "Touch Grass" Submission&lt;/strong&gt;&lt;br&gt;
Tag: &lt;code&gt;#hf26challenge&lt;/code&gt;&lt;br&gt;
Target Prize Categories: &lt;strong&gt;Best Use of TabPFN (Prior Labs)&lt;/strong&gt; ($200) &amp;amp; &lt;strong&gt;Best Use of Gemma (Google)&lt;/strong&gt; ($200)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;💡 What We Built &amp;amp; The Problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Remote workers, engineers, and creators spend an average of 9+ hours a day bathed in unnatural 6500K LED screen glare. This suppresses natural melatonin timing, elevates chronic stress, and leads to screen burnout.&lt;/p&gt;
&lt;p&gt;Most health apps demand &lt;em&gt;more screen time&lt;/em&gt;: tracking food, counting reps, or reading lengthy meditation guides.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SolSpot is designed with one core philosophy: Make the screen the shortest part of the experience.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SolSpot analyzes hyper-local atmospheric and solar data (GHI irradiance, direct blue-spectrum light, UV index, cloud filtering, and thermal indices) to pinpoint your personal &lt;strong&gt;Peak 20-minute "Touch Grass" Window&lt;/strong&gt; of the day.&lt;/p&gt;
&lt;p&gt;When your window arrives, SolSpot launches &lt;strong&gt;Grass Mode&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/YadneshTeli/Solspot" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;(Direct repository link: &lt;a href="https://github.com/YadneshTeli/Solspot" rel="noopener noreferrer"&gt;https://github.com/YadneshTeli/Solspot&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  System Architecture:
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    User([👤 User]) --&amp;gt;|Opens App| UI[🖥️ SolSpot Web UI\nVanilla CSS Glassmorphism]
    UI --&amp;gt;|Local Coordinates| API[⚡ FastAPI Backend on Render]

    API --&amp;gt;|Free Solar Metrics| OM[☀️ Open-Meteo API\nGHI, UV, Temp, Clouds]
    OM --&amp;gt;|Hourly Atmospheric Vectors| TabPFN[🧠 Prior Labs TabPFN\nZero-Shot Tabular Transformer]

    TabPFN --&amp;gt;|Circadian Curve| Best[🌟 Peak 20-Min Window]
    Best --&amp;gt;|Weather Context| Gemma[💎 Google Gemma 2 2B-IT\nSensory Micro-Quest Engine]

    Gemma --&amp;gt; UI
    UI --&amp;gt;|Engage Grass Mode| Lock[🌿 20-Min Fullscreen Horizon Timer]
    Lock --&amp;gt;|Local Session Log| Storage[(🔒 100% Private LocalStorage)]&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;SolSpot is powered by two complementary open-source AI pillars:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. TabPFN (Prior Labs) — Zero-Shot Tabular Foundation Model
&lt;/h3&gt;

&lt;p&gt;Atmospheric chronobiology is tabular data: solar zenith angle, Global Horizontal Irradiance ($W/m^2$), UVB index, cloud cover %, ambient temperature, and wind speed. &lt;br&gt;
Instead of trying to force an LLM to predict tabular curves or manually tuning hyperparameters across classic tree models, we used &lt;strong&gt;Prior Labs' TabPFN&lt;/strong&gt; (&lt;code&gt;tabpfn&amp;gt;=9.1.0&lt;/code&gt;). TabPFN is a transformer pretrained on synthetic tabular datasets that performs instantaneous zero-shot in-context learning. We feed hourly atmospheric vectors directly into &lt;code&gt;TabPFNRegressor&lt;/code&gt; to evaluate circadian daylight scores across the day in milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tabpfn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TabPFNRegressor&lt;/span&gt;

&lt;span class="c1"&gt;# Zero-shot tabular evaluation across hourly atmospheric vectors
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TabPFNRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_circadian_calibration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_wellness_scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Predict circadian daylight score curve for the entire day
&lt;/span&gt;&lt;span class="n"&gt;hourly_scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todays_hourly_matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Google Gemma 2 (2B-IT) — 60-Second Sensory Micro-Quest Engine
&lt;/h3&gt;

&lt;p&gt;Once the window is computed, &lt;strong&gt;Google's Gemma 2&lt;/strong&gt; generates concise, grounding outdoor prompts calibrated to the temperature, cloud cover, and solar angle. It specifically prompts for non-visual senses (smell, skin thermal perception, distant horizon eye relaxation) and explicitly directs the user to close or pocket the screen.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Open-Meteo &amp;amp; Zero-Tracking Architecture
&lt;/h3&gt;

&lt;p&gt;Solar radiation and UV data are fetched from Open-Meteo's open solar API without API keys or tracking IDs. All streak progress is persisted in local storage.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;In a world where big-tech wellness apps monetize your GPS tracking and charge subscription fees for generic advice, an open-source approach fundamentally changes the paradigm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local Privacy by Default&lt;/strong&gt;: You shouldn't have to upload your real-time geolocation or daily routine to an ad-targeting server just to know when to take a walk. Open models and open APIs run privately on your own device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Cloud Tolls &amp;amp; Offline Resilience&lt;/strong&gt;: In the backcountry, on a hiking trail, or in a park with patchy cellular reception, closed cloud APIs fail or introduce latency. Open weights like Gemma 2 and local tabular models like TabPFN run in dead zones with zero per-token API charges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialized Tabular Intelligence&lt;/strong&gt;: LLMs are often misapplied to tabular problems. Prior Labs' open foundation model proves that specialized open transformers deliver vastly superior accuracy for scientific atmospheric data without black-box cloud lock-in.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;This project was planned, scaffolded, and built with AI pair programming using DevRelay. You can explore the full session transcript here:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building SolSpot: Anti-Burnout Natural Light Optimizer with TabPFN and Gemma 2&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;use devrelay mcp and fetch week 1 challenge and suggest what to build&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Analyzed Week 1 'Touch Grass' challenge requirements. Scaffolds SolSpot: an anti-burnout daylight optimizer using Prior Labs TabPFN for zero-shot tabular circadian curve regression and Google Gemma 2 for outdoor sensory micro-quests.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        2 of 2 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Direct session link: &lt;a href="https://dev.to/agent_sessions/building-solspot-anti-burnout-natural-light-optimizer-with-tabpfn-and-gemma-2-zc42sg"&gt;https://dev.to/agent_sessions/building-solspot-anti-burnout-natural-light-optimizer-with-tabpfn-and-gemma-2-zc42sg&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;




</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building DevFeed — Part 1: MVVM Architecture Before Writing a Single Screen</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/building-devfeed-part-1-mvvm-architecture-before-writing-a-single-screen-1g6l</link>
      <guid>https://dev.to/yadneshteli/building-devfeed-part-1-mvvm-architecture-before-writing-a-single-screen-1g6l</guid>
      <description>&lt;p&gt;&lt;em&gt;Series 2 of 2 — Part 1 of 4&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building DevFeed — Part 1: MVVM Architecture Before Writing a Single Screen
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Series: Building DevFeed — A Proper Flutter MVVM App from Day One&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I started DevFeed, I had just finished the painful MVVM refactor of the shopping app. I knew what proper architecture looked like. I knew the difference between a repository and a ViewModel. I had felt the cost of not having that separation.&lt;/p&gt;

&lt;p&gt;So this time I set up the entire architecture before I wrote a single screen.&lt;/p&gt;

&lt;p&gt;DevFeed is a Hacker News reader app built as a Flutter MVVM practice project during my internship. This series documents how I built it — from a properly structured first commit all the way to shipping v1.0.1 with a GitHub Actions release workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning the architecture first
&lt;/h2&gt;

&lt;p&gt;Before opening the first Dart file, I mapped out the layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt;: &lt;code&gt;ArticleModel&lt;/code&gt; to represent a Hacker News story — id, title, url, score, author, and comment count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;code&gt;HackerNewsRepository&lt;/code&gt; responsible for fetching stories from the API. Returns a list of &lt;code&gt;ArticleModel&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ViewModel&lt;/strong&gt;: &lt;code&gt;FeedViewModel&lt;/code&gt; using Riverpod &lt;code&gt;AsyncNotifier&lt;/code&gt;. Calls the repository, exposes &lt;code&gt;AsyncValue&lt;/code&gt; state to the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View&lt;/strong&gt;: &lt;code&gt;FeedScreen&lt;/code&gt; that watches the ViewModel. Renders loading, error, and data states. No API knowledge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same structure I eventually reached in the shopping app. The difference in development experience because of starting with it is the main story of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first commit: full scaffold
&lt;/h2&gt;

&lt;p&gt;The first commit was a full scaffold — all 35 files. Models, repositories, ViewModels, screens, widgets, routing, theming, and a basic test file. Nothing was fully implemented but the skeleton of every layer was in place.&lt;/p&gt;

&lt;p&gt;This approach felt strange at first. You write a lot of boilerplate before you can see anything on screen. But it paid off immediately when I started filling in implementations — because I always knew exactly which file to open and what it should contain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Riverpod AsyncNotifier setup
&lt;/h2&gt;

&lt;p&gt;I used &lt;code&gt;AsyncNotifier&lt;/code&gt; for the feed ViewModel. &lt;code&gt;AsyncNotifier&lt;/code&gt; is the right choice when your state is the result of an async operation — it handles loading, error, and data states out of the box.&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;FeedViewModel&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;AsyncNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArticleModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;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;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArticleModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hackerNewsRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchTopStories&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AsyncLoading&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AsyncValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;guard&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="p"&gt;&amp;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;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hackerNewsRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchTopStories&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;h2&gt;
  
  
  go_router for navigation
&lt;/h2&gt;

&lt;p&gt;I used &lt;code&gt;go_router&lt;/code&gt; from day one. In the shopping app I had used &lt;code&gt;Navigator.push&lt;/code&gt; directly, which scattered navigation logic across screens. &lt;code&gt;go_router&lt;/code&gt; centralises routing in a single config file and supports declarative navigation, deep links, and route guards. Adding a new route later was a two-line change instead of a hunt through multiple screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What starting right feels like
&lt;/h2&gt;

&lt;p&gt;The most noticeable difference compared to the shopping app was the absence of confusion. When I needed to add a feature, I knew where it should go. When I needed to fix a bug, I knew which layer was responsible.&lt;/p&gt;

&lt;p&gt;The shopping app taught me what MVVM is. DevFeed taught me what it feels like to work in a well-structured codebase. You need both lessons.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next: Part 2 — Building the Feed: Hacker News API, AsyncNotifier, and Parallel Fetching&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>architecture</category>
      <category>riverpod</category>
      <category>dart</category>
    </item>
    <item>
      <title>Building a Flutter Shopping App from Scratch — Part 1: Scaffolding Models, Providers, and Screens</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/yadneshteli/building-a-flutter-shopping-app-from-scratch-part-1-scaffolding-models-providers-and-screens-5d0c</link>
      <guid>https://dev.to/yadneshteli/building-a-flutter-shopping-app-from-scratch-part-1-scaffolding-models-providers-and-screens-5d0c</guid>
      <description>&lt;p&gt;&lt;em&gt;Series 1 of 2 — Part 1 of 5&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building a Flutter Shopping App from Scratch — Part 1: Scaffolding Models, Providers, and Screens
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Series: Building a Flutter Shopping App — From Zero to Architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every Flutter developer I know has at some point just started writing code without thinking too much about architecture. I was no different. When my internship mentor gave me a shopping app to build as a learning project, I opened VS Code and started typing.&lt;/p&gt;

&lt;p&gt;This post covers what I built on day one — the models, providers, and screens — and why I made the choices I made, even if some of them turned out to be wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project brief
&lt;/h2&gt;

&lt;p&gt;A standard e-commerce shopping app. Products, categories, a cart, a favourites list, and user authentication. Nothing groundbreaking, but enough to force you to think about state, data flow, and UI structure.&lt;/p&gt;

&lt;p&gt;At this point in my Flutter journey I had a basic understanding of widgets and &lt;code&gt;StatefulWidget&lt;/code&gt;s. I had read about Provider but not really used it properly. I did not know what MVVM was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with models
&lt;/h2&gt;

&lt;p&gt;The first thing I did was define my data structures. A product has a name, price, image URL, and category. A category is just an id and a label.&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;ProductModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;ProductModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;categoryId&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 enough to start with. At this stage I was not thinking about &lt;code&gt;fromJson&lt;/code&gt; or &lt;code&gt;toJson&lt;/code&gt; — that would come back to bite me later when I integrated Firestore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the providers
&lt;/h2&gt;

&lt;p&gt;I used Riverpod for state management. I created providers for products, categories, cart, favourites, and search. Each was a &lt;code&gt;StateNotifier&lt;/code&gt; that held a list and exposed methods to add, remove, and filter items.&lt;/p&gt;

&lt;p&gt;The cart provider looked reasonable at first. But there was a problem I did not see yet — it had no concept of the current user. The cart data was global. If two users logged in on the same device, they would share the same cart. This is the kind of issue you only discover when you try to add authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up screens
&lt;/h2&gt;

&lt;p&gt;I built three screens: &lt;code&gt;HomeScreen&lt;/code&gt;, &lt;code&gt;CartScreen&lt;/code&gt;, and &lt;code&gt;FavouritesScreen&lt;/code&gt;. The &lt;code&gt;HomeScreen&lt;/code&gt; had a product grid, category filters, and a search bar with debounce. The &lt;code&gt;CartScreen&lt;/code&gt; showed cart items with quantities. The &lt;code&gt;FavouritesScreen&lt;/code&gt; was a responsive grid of saved products.&lt;/p&gt;

&lt;p&gt;I also added Device Preview at this stage, which turned out to be a good decision. It caught several responsive layout bugs early without needing to switch simulators.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got right
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using Riverpod from the start&lt;/strong&gt;: Even my naive providers were easier to manage than raw &lt;code&gt;setState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separating models early&lt;/strong&gt;: Gave me a consistent data layer before I understood the full architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding Device Preview early&lt;/strong&gt;: Surfaced layout bugs while they were still cheap to fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global user state&lt;/strong&gt;: Cart and favourites providers had no concept of the current user — state was global.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaking business logic&lt;/strong&gt;: Business logic was leaking into the UI layer — screens called provider methods with too much knowledge of storage internals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tangled data sources&lt;/strong&gt;: No repositories. API calls, local storage reads, and state updates were all tangled inside the same providers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is coming next
&lt;/h2&gt;

&lt;p&gt;In Part 2 I add authentication — Firebase login, signup, Google Sign-In, and the surprisingly tricky problem of merging accounts when a user signs up with email and then tries to log in with Google using the same address.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next: Part 2 — Adding Firebase Auth, Google Sign-In, and Account Merging&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>riverpod</category>
      <category>beginners</category>
    </item>
    <item>
      <title>My Flutter Internship Learning Journey: Two Projects, One Progression</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:03:33 +0000</pubDate>
      <link>https://dev.to/yadneshteli/my-flutter-internship-learning-journey-two-projects-one-progression-103e</link>
      <guid>https://dev.to/yadneshteli/my-flutter-internship-learning-journey-two-projects-one-progression-103e</guid>
      <description>&lt;p&gt;&lt;em&gt;Intro Post&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  My Flutter Internship Learning Journey: Two Projects, One Progression
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A Dev.to blog series about learning Flutter the hard way — and then the right way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I am an MCA student based in Mumbai, currently doing a Flutter developer internship. Over the past few weeks I built two Flutter apps as part of my learning journey. On the surface they look similar — both are Flutter apps, both deal with real features, both use Riverpod. But the way I approached them could not have been more different.&lt;/p&gt;

&lt;p&gt;The first project, a shopping app, started with zero knowledge of proper Flutter architecture. I just started building — models, providers, screens — figuring things out as I went. Halfway through, I realised the codebase was becoming hard to manage. So I stopped and refactored the entire thing to MVVM with repositories and viewmodels. It was painful but I learned more from that refactor than from anything else.&lt;/p&gt;

&lt;p&gt;The second project, DevFeed — a Hacker News reader — is where I applied everything I had just learned. I set up MVVM from the first commit. I never had to do a big painful refactor because the architecture was right from day one. It shipped to v1.0.1 with a changelog and a GitHub Actions release workflow.&lt;/p&gt;

&lt;p&gt;These two projects together tell a story I think a lot of Flutter beginners will relate to. So I am writing about both, as two parallel series.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two series cover
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Series 1 — Building a Flutter Shopping App: From Zero to Architecture&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This follows the shopping app from scratch. You will see me build it the naive way, hit the walls, and then migrate to proper MVVM. If you have ever written Flutter code that became a mess and wondered how to fix it, this series is for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Series 2 — Building DevFeed: A Proper Flutter MVVM App from Day One&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This follows DevFeed from the first commit. No big refactor needed — because the architecture was planned upfront. This series shows what it looks like when you apply the lessons from Series 1 before you start writing features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beyond the Basics: Deep Dives &amp;amp; State Management Alternatives&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Following both projects, I also explore alternative paradigms that broadened my engineering toolkit: GetX, Flutter BLoC, Supabase as a Firebase alternative, implicit &amp;amp; explicit animations, and GraphQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Flutter beginners who are learning by building real projects&lt;/li&gt;
&lt;li&gt;Students doing internships who want to write production-quality code&lt;/li&gt;
&lt;li&gt;Anyone who has built a messy Flutter app and wants to understand MVVM&lt;/li&gt;
&lt;li&gt;Anyone curious about what a real learning-in-public journey looks like&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flutter &amp;amp; Dart&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Riverpod&lt;/strong&gt; for state management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firebase Auth, Firestore, FCM&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MVVM architecture&lt;/strong&gt; with repositories and viewmodels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; for CI/CD automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to follow along
&lt;/h2&gt;

&lt;p&gt;I will post both series in parallel, alternating weekly. Start with Series 1 Part 1 and Series 2 Part 1 simultaneously — they set up the contrast that makes the whole journey make sense.&lt;/p&gt;

&lt;p&gt;Let us get into it.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>DartBox Buddy: Running Flutter On-Device with a Local Gemma 4 AI Tutor for My Friend</title>
      <dc:creator>Yadnesh Teli</dc:creator>
      <pubDate>Mon, 05 Oct 2026 04:32:27 +0000</pubDate>
      <link>https://dev.to/yadneshteli/dartbox-buddy-running-flutter-on-device-with-a-local-gemma-4-ai-tutor-for-my-friend-1g53</link>
      <guid>https://dev.to/yadneshteli/dartbox-buddy-running-flutter-on-device-with-a-local-gemma-4-ai-tutor-for-my-friend-1g53</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A close friend of mine has wanted to learn Flutter for months. They had great app ideas—little personal tools, habit logs, and clean interfaces—but every time they sat down to start, reality hit them like a wall:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Setup Monster&lt;/strong&gt;: First came downloading gigabytes of Android Studio, fighting Gradle daemon memory errors, configuring Java home variables, and waiting 10 minutes for an initial build. By the time the emulator booted, their creative momentum was completely gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Layout Maze&lt;/strong&gt;: When they finally got code running, Flutter layouts proved deceptively tricky. Misunderstanding &lt;code&gt;Row&lt;/code&gt; vs &lt;code&gt;Column&lt;/code&gt; constraints, getting mysterious black-and-yellow striped overflow errors, and not knowing why an element vanished caused endless frustration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Closed AI Trap&lt;/strong&gt;: They tried pasting questions into cloud AI assistants, but web playgrounds only produced browser mockups, closed APIs demanded $20/month subscriptions, and personal daily habit ideas were being shipped to third-party corporate servers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I realized what my friend needed wasn't another thick tutorial or a heavy laptop IDE. They needed a &lt;strong&gt;playful sandbox right in their pocket&lt;/strong&gt;: an app where they could experiment with layouts on the subway, see changes instantly on a real touch screen, and have a patient, private mentor guiding them through every mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DartBox Buddy&lt;/strong&gt; is an on-device Flutter development studio and AI tutor that runs entirely on a mobile phone.&lt;/p&gt;

&lt;p&gt;With DartBox Buddy, you can open any &lt;code&gt;.dart&lt;/code&gt; file on your phone, tap &lt;strong&gt;Run&lt;/strong&gt;, and watch it compile and mount as an interactive, live native Flutter widget in &lt;strong&gt;under 100 milliseconds&lt;/strong&gt;—with &lt;strong&gt;zero project setup, no laptop, no command-line tools, and no internet connection required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sitting beside you in the editor is an on-device AI mentor powered by &lt;strong&gt;Google Gemma 4 E2B&lt;/strong&gt; running via LiteRT-LM. When a layout breaks or a compiler error occurs, Gemma analyzes the AST on your phone, explains the visual hierarchy in plain English, and auto-repairs code before mounting it on screen.&lt;/p&gt;

&lt;p&gt;To solve my friend's layout struggles from day one, DartBox bundles &lt;strong&gt;6 Progressive Layout Lessons&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Centering &amp;amp; Text&lt;/strong&gt;: &lt;code&gt;Center&lt;/code&gt;, &lt;code&gt;Container&lt;/code&gt;, padding, and decoration basics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Padding &amp;amp; Margins&lt;/strong&gt;: Visualizing inner space vs outer margin boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row &amp;amp; Column Alignment&lt;/strong&gt;: Mastering &lt;code&gt;mainAxisAlignment&lt;/code&gt; and &lt;code&gt;crossAxisAlignment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack &amp;amp; Positioned&lt;/strong&gt;: Layering badges, floating tags, and overlapping cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrollable Card Feed&lt;/strong&gt;: Constructing lists with &lt;code&gt;ListView&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, and &lt;code&gt;ListTile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive State&lt;/strong&gt;: Connecting taps to layout updates using &lt;code&gt;StatefulWidget&lt;/code&gt; and &lt;code&gt;setState&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bundled lessons are read-only templates: editing any lesson automatically forks a custom copy into "My Scripts" so original reference lessons are never overwritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&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%2Fj72jjlsjo9ighodl46ta.jpg" 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%2Fj72jjlsjo9ighodl46ta.jpg" alt="DartBox Buddy mobile studio running on a smartphone displaying dark-mode code editor with live Flutter widget preview and on-device Google Gemma 4 AI tutor recommendation pill"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  User Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────┐
│  DartBox Buddy Studio (On-Device Mobile Engine)        │
│                                                        │
│  [Code Editor] ──(Compiles in &amp;lt;100ms)──&amp;gt; [Live Widget] │
│        ▲                                       │       │
│        │                                       ▼       │
│   Auto-Repair &amp;lt;── [On-Device Gemma 4] &amp;lt;── [Errors/AST] │
└────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  My Friend's Reaction
&lt;/h3&gt;

&lt;p&gt;When I handed the phone to my friend and opened Lesson 3 (Row &amp;amp; Column Alignment), their first question was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Wait, where's the laptop? How is this running without Android Studio open?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within five minutes, they had changed the button padding, flipped the alignment from &lt;code&gt;spaceEvenly&lt;/code&gt; to &lt;code&gt;spaceBetween&lt;/code&gt;, and used the &lt;strong&gt;Gemma AI Tutor&lt;/strong&gt; to ask why their text wasn't centered. For the first time, learning Flutter felt as fast and playful as playing a mobile game.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📂 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/yadneshteli/dartbox" rel="noopener noreferrer"&gt;https://github.com/yadneshteli/dartbox&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📜 &lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;AI Model Used&lt;/strong&gt;: &lt;a href="https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm" rel="noopener noreferrer"&gt;Google Gemma 4 E2B LiteRT-LM&lt;/a&gt; (Open-weight, Apache-2.0)&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Built With&lt;/strong&gt;: Flutter, &lt;code&gt;flutter_eval&lt;/code&gt;, &lt;code&gt;flutter_gemma&lt;/code&gt;, LiteRT
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone and run locally&lt;/span&gt;
git clone https://github.com/yadneshteli/dartbox.git
&lt;span class="nb"&gt;cd &lt;/span&gt;dartbox
flutter pub get
flutter run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;DartBox Buddy connects two cutting-edge open technologies:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. The On-Device Flutter Runtime (&lt;code&gt;flutter_eval&lt;/code&gt; + &lt;code&gt;dart_eval&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The host app runs standard Flutter compiled for Android. When the user edits code, DartBox passes the source string to the on-device &lt;code&gt;Compiler&lt;/code&gt; with &lt;code&gt;flutterEvalPlugin&lt;/code&gt;, producing platform-agnostic EVC bytecode in memory. The &lt;code&gt;RuntimeWidget&lt;/code&gt; mounts the bytecode as a live widget tree with interactive touch gestures and state management (&lt;code&gt;setState&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;To ensure my friend wasn't restricted to basic containers, we bridged essential Flutter layout and styling primitives into the dynamic runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layout&lt;/strong&gt;: &lt;code&gt;Flexible&lt;/code&gt;, &lt;code&gt;Wrap&lt;/code&gt;, &lt;code&gt;SingleChildScrollView&lt;/code&gt; for scrollable, wrapping feeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling&lt;/strong&gt;: &lt;code&gt;BoxDecoration&lt;/code&gt;, &lt;code&gt;BorderRadius&lt;/code&gt;, &lt;code&gt;BoxShadow&lt;/code&gt;, and &lt;code&gt;LinearGradient&lt;/code&gt; for sleek card designs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controls &amp;amp; Inputs&lt;/strong&gt;: &lt;code&gt;NavigationBar&lt;/code&gt;, &lt;code&gt;Checkbox&lt;/code&gt;, &lt;code&gt;Switch&lt;/code&gt;, and &lt;code&gt;OutlineInputBorder&lt;/code&gt; for interactive components.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. The On-Device AI Engine (&lt;code&gt;flutter_gemma&lt;/code&gt; + LiteRT-LM)
&lt;/h3&gt;

&lt;p&gt;Inference runs locally using the &lt;code&gt;flutter_gemma&lt;/code&gt; package backed by LiteRT-LM. We deploy &lt;code&gt;gemma-4-E2B-it.litertlm&lt;/code&gt; (an Apache-2.0 open-weight model from the LiteRT community).&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A["Friend edits layout in Editor"] --&amp;gt; B["CompatChecker (Allowlist &amp;amp; Linter)"]
    B --&amp;gt; C["dart_eval Compiler"]
    C --&amp;gt;|Success in &amp;lt;100ms| D["Live Interactive Widget"]
    C --&amp;gt;|Compile Error| E["On-Device Error Console"]

    E --&amp;gt; F["Gemma 4 E2B Tutor (On-Device via LiteRT)"]
    F --&amp;gt;|System Prompts + Widget Hierarchy| G["Explain Error / Layout Hint"]
    G --&amp;gt; H["Self-Healing Repair Loop"]
    H --&amp;gt;|Repaired Code| C&lt;/code&gt;&lt;/pre&gt;


&lt;h3&gt;
  
  
  3. Mobile Ergonomics &amp;amp; Developer Experience
&lt;/h3&gt;

&lt;p&gt;Writing code on a touch keyboard requires deliberate tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-Tap Symbol Bar&lt;/strong&gt;: Instant access to &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;=&amp;gt;&lt;/code&gt;, quotes, and brackets without toggling keyboard layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo &amp;amp; Redo&lt;/strong&gt;: Directly integrated &lt;code&gt;UndoHistoryController&lt;/code&gt; on the mobile toolbar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debounced Auto-Save &amp;amp; Safe Navigation&lt;/strong&gt;: Code automatically saves after 1.5s of inactivity and via &lt;code&gt;PopScope&lt;/code&gt; on back navigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error-to-Line Navigation&lt;/strong&gt;: Tapping any compiler error in the on-device console automatically jumps the cursor to the exact error line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Script Management&lt;/strong&gt;: Local duplicate, rename, delete with confirmation, and clipboard code paste/import.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  4. Benchmarks &amp;amp; Verification
&lt;/h3&gt;

&lt;p&gt;Tested on a &lt;strong&gt;OnePlus 13R (Qualcomm Snapdragon 8 Gen 3, 16 GB RAM)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bytecode Compilation Time&lt;/strong&gt;: &lt;strong&gt;85 ms&lt;/strong&gt; (average for 150-line layout scripts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm Recompile Time&lt;/strong&gt;: &lt;strong&gt;&amp;lt; 15 ms&lt;/strong&gt; (using in-memory AST cache)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Test Suite&lt;/strong&gt;: &lt;strong&gt;16 passed tests&lt;/strong&gt; (100% pass rate across compiler units, all 6 bundled layout lessons, bridge widgets, and the multi-turn Gemma auto-repair loop)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyzer Health&lt;/strong&gt;: 0 errors, 0 warnings with &lt;code&gt;flutter analyze&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packaging&lt;/strong&gt;: Standalone debug APK verified and built cleanly in 68s.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This project was built to prove why &lt;strong&gt;open-source AI&lt;/strong&gt; is transformative for learning and mobile development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100% Offline Independence&lt;/strong&gt;: Learning shouldn't stop when you lose Wi-Fi. By packaging Google's open-weight &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; model (&lt;code&gt;.litertlm&lt;/code&gt;) directly onto the device, the tutor runs in airplane mode, on trains, or in remote spots with zero network latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete Data Sovereignty &amp;amp; Privacy&lt;/strong&gt;: The friend's personal thoughts, draft widgets, and habit data never leave the phone. There is no tracking, no logging, and no telemetry sent to a remote API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Ongoing Costs&lt;/strong&gt;: Cloud LLMs impose steep per-token pricing or monthly subscriptions. Open-weight models democratize access—once installed, millions of tokens of guidance cost exactly $0.00.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic Closed-Loop Compilation&lt;/strong&gt;: Unlike generic cloud chatbots that hallucinate outdated syntax, DartBox pairs Gemma with an on-device compiler (&lt;code&gt;dart_eval&lt;/code&gt; / &lt;code&gt;flutter_eval&lt;/code&gt;). When Gemma suggests or generates a layout, DartBox compiles it in memory. If a syntax mismatch is detected, it feeds the error back to Gemma for an instant &lt;strong&gt;self-healing repair turn&lt;/strong&gt; before the user even sees it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;We saved the AI agent session transcript from our architectural build and test verification using DevRelay:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building DartBox Buddy with On-Device Gemma 4&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Build DartBox Buddy: an on-device Flutter runner and local Google Gemma 4 AI tutor for my friend learning Flutter layouts without laptop setup.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Designing 100% offline architecture: local bytecode compilation via flutter_eval, on-device Gemma 4 E2B inference via LiteRT-LM, debounced auto-save, and self-healing auto-repair loop.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-runcommand"&gt;run_command&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;flutter test&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ flutter test&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;All 16 tests passed cleanly! Bytecode compilation, bundled lessons, and Gemma repair loop verified.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        2 of 2 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct Transcript Link&lt;/strong&gt;: &lt;a href="https://dev.to/agent_sessions/building-dartbox-buddy-with-on-device-gemma-4-deqbkr"&gt;DevRelay Agent Session #543&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent session covers designing the 100% offline runtime architecture, extending &lt;code&gt;flutter_eval&lt;/code&gt; with layout bridges, implementing the 3-turn Gemma auto-repair loop, and passing all 16 automated tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Featured Category: Best Use of Gemma ($200)
&lt;/h3&gt;

&lt;p&gt;DartBox Buddy enters the &lt;strong&gt;Best Use of Gemma&lt;/strong&gt; category. The project features Google's open-weight &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; model (&lt;code&gt;gemma-4-E2B-it.litertlm&lt;/code&gt;, Apache-2.0) running natively on-device via LiteRT-LM. &lt;/p&gt;

&lt;p&gt;Gemma serves as an on-device tutor and autonomous code repair engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visual Hierarchy Explanation&lt;/strong&gt;: Deconstructs Flutter widget trees and explains layout rules in plain English without cloud latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Healing Auto-Repair&lt;/strong&gt;: Automatically catches compiler errors, feeds AST error diagnostics into an on-device 3-turn repair loop, and outputs corrected code that compiles and mounts live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Privacy&lt;/strong&gt;: Ensures all user code, draft widgets, and learning conversations remain 100% local on the device.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Thank you to DEV, Google, and the Hacktoberfest organizers for this challenge!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>weekendchallenge</category>
      <category>devchallenge</category>
      <category>gemma</category>
    </item>
  </channel>
</rss>
