<?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: Gulshan Yadav</title>
    <description>The latest articles on DEV Community by Gulshan Yadav (@mryadavgulshan).</description>
    <link>https://dev.to/mryadavgulshan</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%2F2134389%2Fc9521ffc-977a-4dd5-8b72-0d7af84408c4.jpg</url>
      <title>DEV Community: Gulshan Yadav</title>
      <link>https://dev.to/mryadavgulshan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mryadavgulshan"/>
    <language>en</language>
    <item>
      <title>Building a Responsive Flutter UI That Never Breaks</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/building-a-responsive-flutter-ui-that-never-breaks-4j49</link>
      <guid>https://dev.to/mryadavgulshan/building-a-responsive-flutter-ui-that-never-breaks-4j49</guid>
      <description>&lt;p&gt;&lt;em&gt;LayoutBuilder, breakpoints, and adaptive widgets — a system that survives every screen size, including the ones you didn't test.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first Flutter app I shipped looked perfect — in the Android Studio emulator, at one fixed resolution. The first person to install it on a real phone had a narrow device, and half the dashboard overflowed with the classic yellow-and-black striped error bars. I fixed that bug, and then the next overflow, and the next, every time someone rotated the screen or resized the window.&lt;/p&gt;

&lt;p&gt;So, in this article, I will be showing you how I build responsive Flutter UIs now — the ones that do not break on any screen, orientation, or text scale, because I stopped guessing sizes and started building a small, boring system.&lt;/p&gt;

&lt;p&gt;The good news first: &lt;strong&gt;you need zero extra dependencies.&lt;/strong&gt; Responsiveness in Flutter is built into the framework. What you need is the discipline to use the right tools in the right places. Let me show you exactly which tools, in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rule: Let Constraints Drive, Never Absolute Pixels
&lt;/h2&gt;

&lt;p&gt;The single habit that fixes most breakage: stop using absolute sizes for anything that can vary. No &lt;code&gt;SizedBox(width: 400)&lt;/code&gt;, no hard-coded card widths, no &lt;code&gt;fontSize: 20&lt;/code&gt; on body text that needs to scale with the user's accessibility settings. If a size depends on the screen, the screen should decide — and Flutter gives you exactly three ways to let it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: LayoutBuilder — Size Comes From Your Parent
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LayoutBuilder&lt;/code&gt; gives you the constraints your parent actually imposes, at build time. This is the workhorse of responsive layout. Here is the breakpoint switch I use in almost every screen:&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;ResponsiveScaffold&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ResponsiveScaffold&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="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;LayoutBuilder&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;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constraints&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;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxWidth&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;width&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;WideLayout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// desktop&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;width&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;700&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MediumLayout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// tablet / landscape&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NarrowLayout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// phone&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three layouts, one decision point. When you open this file, you can see the whole responsive story of the screen at a glance. The breakpoints (1200 / 700) are not magic numbers — they are where your layout actually stops working, and you should tune them to your design, not to a blog post.&lt;/p&gt;

&lt;p&gt;Inside a layout, use the same pattern at a finer grain. A detail panel that should sit beside content on wide screens and below it on narrow ones:&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;LayoutBuilder&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;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constraints&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;showSidebar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxWidth&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&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;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;content&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;showSidebar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Flexible and Expanded — Sharing Space, Not Fighting Over It
&lt;/h2&gt;

&lt;p&gt;Half the overflows I debugged came from fixed-size children inside flexible parents. &lt;code&gt;Expanded&lt;/code&gt; and &lt;code&gt;Flexible&lt;/code&gt; are how you tell a child, "take the space the parent decided to give you, and make it work." The rule I live by: &lt;strong&gt;a Row or Column of dynamic content should have exactly one child that can't be squeezed — and the rest should be flexible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The distinction between the two matters more than people think. &lt;code&gt;Expanded&lt;/code&gt; is a &lt;code&gt;Flexible&lt;/code&gt; with &lt;code&gt;fit: FlexFit.tight&lt;/code&gt; — the child &lt;em&gt;must&lt;/em&gt; fill the assigned space, even if that means being stretched past its natural size. &lt;code&gt;Flexible&lt;/code&gt; with &lt;code&gt;FlexFit.loose&lt;/code&gt; gives the child the space as an &lt;em&gt;upper bound&lt;/em&gt;: it can take less if it does not need more. For a title row you almost always want &lt;code&gt;Flexible&lt;/code&gt; on the text side and &lt;code&gt;Expanded&lt;/code&gt; only when you genuinely need the child to fill every pixel. Text widgets also get an explicit &lt;code&gt;flex&lt;/code&gt; factor when you want them to share proportionally — a &lt;code&gt;Flexible(flex: 2)&lt;/code&gt; and a &lt;code&gt;Flexible(flex: 1)&lt;/code&gt; split leftover space 2:1, which is how I do two-column info rows without any fixed widths.&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;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;radius:&lt;/span&gt; &lt;span class="mi"&gt;24&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;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;crossAxisAlignment:&lt;/span&gt; &lt;span class="n"&gt;CrossAxisAlignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;maxLines:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;overflow:&lt;/span&gt; &lt;span class="n"&gt;TextOverflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ellipsis&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;maxLines:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;overflow:&lt;/span&gt; &lt;span class="n"&gt;TextOverflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ellipsis&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trailing&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="n"&gt;trailing&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the details that keep this from breaking: the titles are capped with &lt;code&gt;maxLines&lt;/code&gt; and &lt;code&gt;TextOverflow.ellipsis&lt;/code&gt;, so a long name or a large font scale cannot push the row out of bounds. &lt;code&gt;Expanded&lt;/code&gt; takes the remaining width; the trailing widget keeps its natural size. If you leave every child unconstrained and just hope, the text will win and the row will break.&lt;/p&gt;

&lt;p&gt;For grids and lists, the same idea: &lt;code&gt;GridView&lt;/code&gt; with &lt;code&gt;SliverGridDelegateWithMaxCrossAxisExtent&lt;/code&gt; lets the number of columns be decided by available width instead of by you:&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;GridView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;gridDelegate:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SliverGridDelegateWithMaxCrossAxisExtent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;maxCrossAxisExtent:&lt;/span&gt; &lt;span class="mi"&gt;260&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;childAspectRatio:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;itemBuilder:&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="n"&gt;index&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;ProductCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;index:&lt;/span&gt; &lt;span class="n"&gt;index&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;On a phone you get 2 columns, on a tablet 4, on a desktop 6 — and you wrote one line. The framework counts columns for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: MediaQuery — The App-Level Facts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LayoutBuilder&lt;/code&gt; tells you about &lt;em&gt;space&lt;/em&gt;. &lt;code&gt;MediaQuery&lt;/code&gt; tells you about the &lt;em&gt;device&lt;/em&gt; — orientation, text scale, platform, and safe areas. Use it for decisions that are about the environment, not 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;final&lt;/span&gt; &lt;span class="n"&gt;mq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MediaQuery&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orientation&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Orientation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;landscape&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;mq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;height&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LandscapeCompact&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// desktop-style layout on a phone turned sideways&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the one I add to every scaffold: &lt;code&gt;SafeArea&lt;/code&gt;. The modern phone cutout, the home indicator, and the notch will eat your content on real devices even though the emulator never shows them. Wrap anything that can sit near the edge:&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;return&lt;/span&gt; &lt;span class="nf"&gt;SafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the orientation decision specifically, &lt;code&gt;OrientationBuilder&lt;/code&gt; is sometimes the cleaner tool than reading &lt;code&gt;MediaQuery&lt;/code&gt; — it rebuilds exactly when the &lt;em&gt;orientation&lt;/em&gt; flips, rather than on every size change, which keeps it cheap:&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;OrientationBuilder&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;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orientation&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;orientation&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Orientation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;portrait&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SettingsList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Controls&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;SettingsList&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;Controls&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;Use &lt;code&gt;OrientationBuilder&lt;/code&gt; when your layout decision is literally "portrait vs landscape." Use &lt;code&gt;LayoutBuilder&lt;/code&gt; when the actual width or height numbers matter. The two overlap, but they answer different questions, and mixing them thoughtfully is what separates a responsive app from a fragile one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Adaptive Widgets — Same Logic, Different Widget
&lt;/h2&gt;

&lt;p&gt;Responsive is not just about size; it is about choosing the right widget for the platform and context. Flutter's material library gives you widgets that pick for you. &lt;code&gt;SelectableRegion&lt;/code&gt; aside, the ones I reach for daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NavigationRail&lt;/code&gt; vs &lt;code&gt;NavigationBar&lt;/code&gt;&lt;/strong&gt; — a rail on wide screens, a bottom bar on phones. The &lt;code&gt;adaptive&lt;/code&gt; variants handle the switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Dialog&lt;/code&gt; vs &lt;code&gt;AlertDialog&lt;/code&gt;&lt;/strong&gt; — trivial, but the adaptive dialog picks the platform style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ListView&lt;/code&gt; vs &lt;code&gt;Wrap&lt;/code&gt;&lt;/strong&gt; — when item counts are unknown, a &lt;code&gt;Wrap&lt;/code&gt; flows instead of overflowing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the pattern I actually ship — a single &lt;code&gt;AdaptiveProductGrid&lt;/code&gt; that switches layout based on width using only core widgets:&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;AdaptiveProductGrid&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AdaptiveProductGrid&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="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;LayoutBuilder&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;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constraints&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;isWide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxWidth&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;700&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;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;crossAxisAlignment:&lt;/span&gt; &lt;span class="n"&gt;CrossAxisAlignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Featured'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;isWide&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Theme&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;textTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headlineMedium&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Theme&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;textTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;titleLarge&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;isWide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                  &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;_Card&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;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;_Card&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;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;_Card&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="k"&gt;else&lt;/span&gt;
              &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;separated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;shrinkWrap:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;physics:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NeverScrollableScrollPhysics&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;separatorBuilder:&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="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;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nl"&gt;itemBuilder:&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="n"&gt;i&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;_Card&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;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;One screen, two arrangements, driven entirely by the parent's constraints. This is the whole method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important Notes and Pitfalls
&lt;/h2&gt;

&lt;p&gt;These are the traps I hit so you do not have to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MediaQuery.of(context)&lt;/code&gt; used for layout decisions deep in the tree&lt;/strong&gt; is fine, but for &lt;em&gt;per-widget&lt;/em&gt; sizing always prefer &lt;code&gt;LayoutBuilder&lt;/code&gt; — the media query tells you about the whole screen, not the space your widget actually has inside a column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard-coded heights still break everything.&lt;/strong&gt; If a card has &lt;code&gt;height: 240&lt;/code&gt; and the text inside scales up for an accessibility setting, you get overflow. Use &lt;code&gt;IntrinsicHeight&lt;/code&gt; sparingly (it is expensive) or let content define height with flex containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotating the phone is a different layout pass.&lt;/strong&gt; Test landscape — a phone rotated is narrower in height than a desktop, and a wide row that worked in portrait will overflow in landscape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text scaling is the silent killer.&lt;/strong&gt; Users with large font settings will break your carefully measured UI even though every widget is "flexible." Always cap text with &lt;code&gt;maxLines&lt;/code&gt; + &lt;code&gt;overflow&lt;/code&gt;, and never hard-code &lt;code&gt;fontSize&lt;/code&gt; on text that must scale — use &lt;code&gt;MediaQuery.textScaler&lt;/code&gt; or the theme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizes inside an &lt;code&gt;Expanded&lt;/code&gt; are still fixed.&lt;/strong&gt; &lt;code&gt;Expanded&lt;/code&gt; gives the child a width, but the child can still overflow vertically if you set a fixed height and the content is taller. The pair that works is flex on the axis you want to survive and &lt;code&gt;clip&lt;/code&gt;/&lt;code&gt;scroll&lt;/code&gt; on the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on the smallest device you plan to support, not the prettiest.&lt;/strong&gt; If the breakpoint logic is right, the small phone is where it proves itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Questions I Get Every Time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Do I need a package like &lt;code&gt;responsive_framework&lt;/code&gt;?"&lt;/strong&gt; No. For 90% of apps, the core toolkit above is enough, and every package you add is a constraint you will fight later. Packages help when you want media-query-style breakpoint helpers across a huge app; by the time you need that, you also know exactly why, and you can evaluate them on their merits. Start dependency-free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Where should the breakpoint numbers live?"&lt;/strong&gt; One constants file. Put &lt;code&gt;1200&lt;/code&gt;, &lt;code&gt;900&lt;/code&gt;, &lt;code&gt;700&lt;/code&gt; in named constants like &lt;code&gt;kTabletBreakpoint&lt;/code&gt; and &lt;code&gt;kDesktopBreakpoint&lt;/code&gt;, with a comment explaining what breaks at each width. Magic numbers scattered across 40 widgets are how "we tuned it once" becomes "we tuned it forty times, inconsistently."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What about very wide desktop windows?"&lt;/strong&gt; Cap the content, do not stretch it. A &lt;code&gt;ConstrainedBox&lt;/code&gt; with &lt;code&gt;maxWidth: 1200&lt;/code&gt; centered on screen reads far better than a dashboard that spans 2,500 pixels of ultrawide. Wider is not better past the point where the eye has to travel too far.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do I handle tablets with weird aspect ratios?"&lt;/strong&gt; Same as everything else — by width, not by device model. Foldables, split-screen multitasking, and desktop windows all produce arbitrary widths, and a width-driven layout handles all of them with the same three branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Method, Compressed
&lt;/h2&gt;

&lt;p&gt;Here is the decision rule I hand to anyone on my team, in order of preference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Let the parent decide.&lt;/strong&gt; &lt;code&gt;LayoutBuilder&lt;/code&gt; at the top of the screen; choose a layout from the available width.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never fight the space.&lt;/strong&gt; &lt;code&gt;Expanded&lt;/code&gt; / &lt;code&gt;Flexible&lt;/code&gt; for the dynamic content; cap text with &lt;code&gt;maxLines&lt;/code&gt; and ellipsis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the device for facts.&lt;/strong&gt; &lt;code&gt;MediaQuery&lt;/code&gt; for orientation and text scale; &lt;code&gt;SafeArea&lt;/code&gt; for real-device edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the framework count.&lt;/strong&gt; &lt;code&gt;SliverGridDelegateWithMaxCrossAxisExtent&lt;/code&gt; and adaptive material widgets instead of manual math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove it.&lt;/strong&gt; Run the screen at every breakpoint width, both orientations, and the largest text scale your OS offers — that ten-minute pass catches the regressions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Done that way, "responsive" stops being a heroic refactor and becomes the default. No overflows, no fixed-size assumptions, no surprises when someone installs the app on a phone you never owned.&lt;/p&gt;

&lt;p&gt;I have also written about adaptive navigation and platform-adaptive theming — comment below with your own responsive horror story, or the screen that keeps breaking for you, and I will cover it next.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
    <item>
      <title>Running LLMs Without a GPU: What's Actually Possible</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Mon, 31 Aug 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/running-llms-without-a-gpu-whats-actually-possible-1b7p</link>
      <guid>https://dev.to/mryadavgulshan/running-llms-without-a-gpu-whats-actually-possible-1b7p</guid>
      <description>&lt;p&gt;&lt;em&gt;The honest numbers on CPU, NPU, and iGPU inference — what runs, how fast, and when to stop pretending.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last year a friend who runs a small consulting firm in Dubai asked me what GPU he needed to "run AI locally." He had a budget in mind and had already been quoted a four-figure price tag for a workstation. I asked him what he actually wanted to run. A document chatbot over his firm's contracts. He owned a three-year-old office laptop with 16 GB of RAM and no discrete GPU.&lt;/p&gt;

&lt;p&gt;I told him not to buy anything yet. Two hours later, I had a 7B-parameter model running on that laptop at about 11 tokens per second, answering questions from his own PDFs. Not fast enough for a chat product serving a thousand users. Fast enough for a private assistant that costs nothing per query and keeps every contract on his machine.&lt;/p&gt;

&lt;p&gt;The market wants you to believe local AI requires expensive hardware. The truth is more interesting and much cheaper. Here is what is actually possible — with real numbers, real architecture, and an honest map of where CPU-only inference stops being viable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Everyone Thinks You Need a GPU
&lt;/h2&gt;

&lt;p&gt;The confusion comes from a conflation of two different workloads. Training and fine-tuning a model is brutally compute-hungry: it means doing forward passes over billions of parameters millions of times, and backpropagation on top. That genuinely wants GPUs, and a lot of them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Inference&lt;/em&gt; — running the model to produce an answer — is a different animal. Generating one token means one forward pass through the network. It is memory-bandwidth bound far more than compute bound, especially at the small batch sizes a single user generates. That is the single most important fact in this entire article: &lt;strong&gt;for single-user inference, what matters is how fast you can stream weights from memory, not how many FLOPS you have.&lt;/strong&gt; A CPU with lots of fast RAM can do a surprisingly respectable job, and modern processors have neural accelerators that help even more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Runs, and How Fast: The Honest Numbers
&lt;/h2&gt;

&lt;p&gt;Here are the real token rates I have measured on actual hardware, not the marketing ones. Your mileage varies, but these are the right ballparks for current consumer machines:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Model &amp;amp; quantization&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2021 office laptop, 16 GB RAM&lt;/td&gt;
&lt;td&gt;Llama-3.1-8B, Q4_K_M&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;~10–12 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple M2/M3 (MacBook Air)&lt;/td&gt;
&lt;td&gt;Llama-3.1-8B, Q4_K_M&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;~25–35 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple M2 Pro/Max (memory bandwidth)&lt;/td&gt;
&lt;td&gt;14B, Q4_K_M&lt;/td&gt;
&lt;td&gt;~9 GB&lt;/td&gt;
&lt;td&gt;~20–30 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recent laptop CPU with NPU (Intel/AMD/Qualcomm)&lt;/td&gt;
&lt;td&gt;7–8B, Q4, NPU offload&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;15–30 tok/s, lower power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDR5 desktop CPU (16 cores, no GPU)&lt;/td&gt;
&lt;td&gt;8B, Q4_K_M&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;15–20 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same desktop, 32 GB RAM&lt;/td&gt;
&lt;td&gt;14B Q4_K_M&lt;/td&gt;
&lt;td&gt;~9 GB&lt;/td&gt;
&lt;td&gt;8–12 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iGPU (shared memory) offload&lt;/td&gt;
&lt;td&gt;8B, Q4_K_M&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;modest gain over CPU alone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For context: comfortable reading speed is roughly 20 tokens per second. Below 8 tokens per second, interactive chat starts to feel sluggish, though a &lt;em&gt;batch&lt;/em&gt; job — summarize these 400 documents overnight — does not care about interactivity at all, which changes the calculus completely.&lt;/p&gt;

&lt;p&gt;One more distinction that explains why CPU inference feels uneven in practice: the two phases of generation behave very differently. &lt;strong&gt;Prompt processing&lt;/strong&gt; (reading the input, aka prefill) is compute-bound and can be slow on CPU for long inputs — a 2,000-token prompt might take a second or two before the first output token appears. &lt;strong&gt;Token generation&lt;/strong&gt; (decode) is memory-bandwidth-bound and steady — once the model is warmed up, each output token streams at a consistent rate. So the experienced quality of a CPU model is dominated by prompt length: short prompts with long answers feel fine; long prompts with short answers feel sluggish, and that is pure prefill time, not generation speed. Capping context and keeping inputs tight is not a quality compromise, it is a latency lever.&lt;/p&gt;

&lt;p&gt;The magic ingredient on the Apple side is unified memory: the same memory serves CPU and GPU, and it is &lt;em&gt;fast&lt;/em&gt; memory. That is why a fanless MacBook Air outperforms a bigger Windows laptop for llama.cpp. On the Windows/Intel side, the NPUs shipping in 2024+ laptops are the emerging story — they are built for low-power token generation, and software support is maturing quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack That Makes This Work
&lt;/h2&gt;

&lt;p&gt;Every serious CPU inference setup is built from the same four pieces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Quantized models (GGUF).&lt;/strong&gt; The breakthrough that made CPU inference practical. You take a model whose weights are 16-bit floats and shrink them to 4-bit integers, trading a little quality for a ~4x memory and bandwidth reduction. The GGUF format (from the llama.cpp project) is the standard container. The Q4_K_M variant is the sweet spot I default to — decent quality, ~5 GB for an 8B model, fits in a laptop's budget. Q8_0 is better quality at ~9 GB; Q3_K_M fits in 4 GB but you will feel the quality loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. llama.cpp.&lt;/strong&gt; The reference runtime. A pure C/C++ implementation that runs on CPU, and offloads layers to GPU/NPU where available. It is the engine behind virtually every local-LLM tool you have heard of, and it is fast on CPU because it was written to be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A server layer.&lt;/strong&gt; llama.cpp ships a server binary that speaks the OpenAI-compatible chat/completions API. This is the detail that makes CPU inference actually usable: any tool that talks to the OpenAI API — including your existing app, if you point its &lt;code&gt;base_url&lt;/code&gt; at your local server — will work against your CPU model with a one-line change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Ollama (or llama-cpp-python) for the glue.&lt;/strong&gt; Ollama wraps llama.cpp with model management and a dead-simple CLI; &lt;code&gt;llama-cpp-python&lt;/code&gt; gives you the same engine as a Python package. Both are legitimate; you are choosing convenience over control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Actually Run One
&lt;/h2&gt;

&lt;p&gt;Here is the whole setup on a Mac or Linux box. First, install Ollama and pull a quantized model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install ollama, then:&lt;/span&gt;
ollama pull llama3.1:8b-q4_K_M
ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single command is now a running LLM endpoint on &lt;code&gt;http://localhost:11434&lt;/code&gt;, OpenAI-compatible. Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:11434/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "llama3.1:8b-q4_K_M",
       "messages": [{"role": "user", "content": "Explain quantization in 50 words."}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer raw llama.cpp for control — say, you want to offload specific layers to an iGPU or NPU — you clone it and run the server directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ggml-org/llama.cpp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;llama.cpp
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;--config&lt;/span&gt; Release
./build/bin/llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-m&lt;/span&gt; llama-3.1-8b-instruct-Q4_K_M.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--n-gpu-layers&lt;/span&gt; 12 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--n-gpu-layers&lt;/code&gt; flag is the whole game on hybrid hardware: you push the layers that benefit from the GPU/NPU and keep the rest on CPU, trading power draw for speed. On the Apple side the equivalent is Metal layers (&lt;code&gt;-ngl&lt;/code&gt; with Metal enabled).&lt;/p&gt;

&lt;p&gt;Now the part people forget. Point your OpenAI-compatible client at it:&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;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3.1:8b-q4_K_M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize the attached contract risk clauses.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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 it. Your app now runs on a local model with zero per-query cost and zero data leaving the machine. The entire "I need a GPU" story collapses into one &lt;code&gt;base_url&lt;/code&gt; change.&lt;/p&gt;

&lt;p&gt;If you do not know which model to start with, here is the shortlist I use. For a first experiment, a 7–8B model at Q4_K_M (Llama 3.1, Qwen 2.5, Gemma) is the right default on 16 GB of RAM — big enough to feel genuinely capable, small enough to be usable. If you are on 8 GB or an older machine, step down to a 3–4B model at Q4_K_M; the quality gap is smaller than the speed gap suggests. And if you only need classification or extraction, a 1–3B model at 40+ tok/s will beat an 8B at 10 tok/s for the job. The rule: pick the smallest model that passes your acceptance test, then measure — most people overestimate what they need by one size.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes CPU Inference Go Fast (or Slow)
&lt;/h2&gt;

&lt;p&gt;If you are on CPU-only and the speed is not acceptable, the fix is almost never "get a GPU." Work through this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check your quantization.&lt;/strong&gt; Going from Q8_0 to Q4_K_M nearly doubles throughput at modest quality cost. For chat, most people cannot reliably tell the difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap your context.&lt;/strong&gt; A long conversation balloons the prompt-processing phase. Dropping &lt;code&gt;-c&lt;/code&gt; from 8192 to 4096 makes the first token arrive much faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use your accelerators.&lt;/strong&gt; Offload layers to the iGPU/NPU where available. Even a partial offload cuts power draw and wall time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the model to the job.&lt;/strong&gt; A 3B model at 40 tok/s is better than an 8B at 9 tok/s for classification, extraction, or summarization. Use a small model for the routine work and escalate only when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache.&lt;/strong&gt; KV-cache reuse and prompt caching cut latency dramatically on repeated queries. llama.cpp supports both.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And here is the counterintuitive one: for &lt;strong&gt;batch work&lt;/strong&gt;, a slow model is a non-issue. If you need to tag 10,000 support tickets overnight, 5 tok/s is plenty. You are optimizing for throughput over time, not first-token latency, and CPU inference wins that comparison on cost by a mile.&lt;/p&gt;

&lt;p&gt;The same logic extends to &lt;strong&gt;embeddings&lt;/strong&gt;, which people forget entirely. An embedding model like a small 300M–1B parameter encoder runs happily on CPU and is the backbone of local RAG: it turns your documents into vectors on your own machine, with no API calls and no data leaving the building. I have run a full local retrieval pipeline — embeddings plus a small chat model — on a single 16 GB laptop, and the retrieval step, which is the part that actually makes answers good, is nearly instant. If your goal is a private document assistant, the LLM is the part you have to accept as "good enough"; the embeddings are the part that genuinely shines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Breaks: The Honest Limits
&lt;/h2&gt;

&lt;p&gt;I want to be equally honest about the wall. CPU-only inference fails in four specific places, and you should not pretend otherwise:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Large models.&lt;/strong&gt; Anything above ~14B at Q4 needs 10+ GB just for weights, and you will hit 5–8 tok/s — below the interactive threshold. Models that need 32 GB of RAM are off the table entirely on consumer hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High concurrency.&lt;/strong&gt; A CPU serving five users at once just divides its tokens between them. This is a single-user story, or a low-concurrency internal tool, not a public product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-generation tasks.&lt;/strong&gt; Drafting a full article or a 2,000-token email response is painful at 10 tok/s — 3+ minutes. Plan for it or use a small, fast model for drafting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent workloads.&lt;/strong&gt; When an agent runs ten chained model calls with tool calls in between, each one pays the full prompt-processing tax. On CPU, a five-step agent run can take minutes. Local-first agents on CPU exist, but you are optimizing for privacy and cost, not speed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When You Actually Do Need a GPU
&lt;/h2&gt;

&lt;p&gt;You need the GPU when the workload is compute-bound or concurrent: fine-tuning, large-scale embedding jobs, or serving many simultaneous users. If you are building the next consumer chatbot product, you are not the CPU story. You are the &lt;em&gt;cloud API&lt;/em&gt; story.&lt;/p&gt;

&lt;p&gt;But here is the framing I use now, after years of building: &lt;strong&gt;the GPU question is not "should I buy one" — it is "what is the workload, what is the concurrency, and what are the latency requirements?"&lt;/strong&gt; A private document assistant, a code-completion tool running on a single developer's machine, an on-prem compliance filter that must never see the internet, a batch tagging pipeline — all of these are CPU-viable today, and the hardware you already own is probably enough to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practitioner's Checklist
&lt;/h2&gt;

&lt;p&gt;When someone asks me to stand up local LLMs on the hardware they have, I work through this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Define the workload: interactive chat, batch processing, or agent loops&lt;/li&gt;
&lt;li&gt;[ ] Measure RAM: weights (Q4 ≈ 0.6–0.7 GB per billion params) plus context, plus OS&lt;/li&gt;
&lt;li&gt;[ ] Pick GGUF quantization: Q4_K_M default, Q8_0 if quality matters more than speed&lt;/li&gt;
&lt;li&gt;[ ] Start with the biggest model that fits in RAM at the target speed; drop a size if under ~8 tok/s for chat&lt;/li&gt;
&lt;li&gt;[ ] Set the server up OpenAI-compatible so existing code works with a &lt;code&gt;base_url&lt;/code&gt; change&lt;/li&gt;
&lt;li&gt;[ ] Cap context to what the task actually needs&lt;/li&gt;
&lt;li&gt;[ ] Offload layers to iGPU/NPU/Metal where available&lt;/li&gt;
&lt;li&gt;[ ] For batch jobs, accept low token rates and let throughput win&lt;/li&gt;
&lt;li&gt;[ ] For agents, budget for multi-call latency or drop to a smaller model&lt;/li&gt;
&lt;li&gt;[ ] Keep a cloud API as the escape hatch for the jobs that outgrow the box&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real Answer
&lt;/h2&gt;

&lt;p&gt;My friend in Dubai runs his contract assistant on that same laptop today. It does not write poetry and it is not answering a thousand concurrent users. It reads contracts, answers questions in context, and costs him nothing per query — and his documents never leave his machine. That is the actual value proposition of CPU inference: not "local models beat the cloud," but "local models are good enough for a specific class of work, and that class is much bigger than people think."&lt;/p&gt;

&lt;p&gt;Start with what you already own. Quantize, cap context, offload what you can, and match the model to the job. You will be surprised how far a laptop goes — and you will only spend GPU money when the workload genuinely demands it.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>technology</category>
    </item>
    <item>
      <title>Flutter Provider vs Riverpod vs Bloc — Which Should You Learn?</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Sun, 30 Aug 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/flutter-provider-vs-riverpod-vs-bloc-which-should-you-learn-944</link>
      <guid>https://dev.to/mryadavgulshan/flutter-provider-vs-riverpod-vs-bloc-which-should-you-learn-944</guid>
      <description>&lt;p&gt;&lt;em&gt;The question every Flutter developer asks, answered with a framework instead of a fanboy answer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I get the same question at least twice a month, in comments, in DMs, from junior devs at meetups: "Provider, Riverpod, or Bloc — which one should I learn?" It is the Flutter equivalent of asking which framework to learn first in JavaScript, and the answer is usually whoever answered last. That is not how I answer it anymore, because I have shipped Flutter apps with all three — some for clients, some for my own products — and each one of them was the right choice in a different context.&lt;/p&gt;

&lt;p&gt;Here is the honest version. Not the version where the latest package wins because it is newer. The version where I tell you exactly what each solution costs you and when it pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Question Is Hard to Answer
&lt;/h2&gt;

&lt;p&gt;State management is the first real architectural decision a Flutter developer makes, and the ecosystem makes it harder than it needs to be. All three of these solutions work. All three are actively maintained, documented, and power real production apps. You will not ship a broken app because you picked Provider over Bloc.&lt;/p&gt;

&lt;p&gt;So the real question is not "which is best" — it is "which should you learn &lt;em&gt;first&lt;/em&gt;, and which should you &lt;em&gt;use&lt;/em&gt;, given what you are building and who will maintain it." Those are different answers, and conflating them is where developers waste months. I spent a year on the wrong answer myself, so let me save you the detour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Criteria
&lt;/h2&gt;

&lt;p&gt;Before the comparison, here are the six criteria I actually judge state management on when I recommend it to a client or a mentee. They are ranked in the order they bite you in production:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;What it really means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Days until a new dev can ship a feature, not a demo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate&lt;/td&gt;
&lt;td&gt;Lines of code per feature — and lines you can't avoid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;How easy it is to test business logic without a widget tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem &amp;amp; tooling&lt;/td&gt;
&lt;td&gt;DevTools support, community packages, debugging experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor cost&lt;/td&gt;
&lt;td&gt;What it costs to migrate away if you outgrow it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team fit&lt;/td&gt;
&lt;td&gt;How readable it is to the people who will maintain it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now let me score each solution against those, honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider — The Gateway Drug, and Proud of It
&lt;/h2&gt;

&lt;p&gt;Provider is what most Flutter developers learn first, because it is what the official docs steered you toward for years. It is a thin wrapper over &lt;code&gt;InheritedWidget&lt;/code&gt;, and that is its superpower: it does one thing, and it does it well. You expose a value to the widget tree with &lt;code&gt;Provider.of&lt;/code&gt; or &lt;code&gt;Consumer&lt;/code&gt;, and widgets rebuild when that value changes.&lt;/p&gt;

&lt;p&gt;The code is refreshingly small:&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;CartProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ChangeNotifier&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CartItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&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;CartItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unmodifiable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CartItem&lt;/span&gt; &lt;span class="n"&gt;item&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;_items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// expose it&lt;/span&gt;
&lt;span class="n"&gt;ChangeNotifierProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;create:&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;CartProvider&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// consume it&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&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;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CartProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strengths are real. There is almost no ceremony: no events, no states, no generators. A junior developer can read Provider code the afternoon they learn it, which makes it an excellent default for small-to-medium apps and for teams that do not want to hire around a paradigm. The DevTools integration is solid, and &lt;code&gt;Provider&lt;/code&gt;'s companion packages (&lt;code&gt;ChangeNotifierProvider&lt;/code&gt;, &lt;code&gt;MultiProvider&lt;/code&gt;, &lt;code&gt;ProxyProvider&lt;/code&gt;) cover most needs without pulling in anything heavy.&lt;/p&gt;

&lt;p&gt;The weaknesses are the flip side of the simplicity. Because the model combines state and notification, logic and presentation get tangled fast as the app grows. There is no enforced structure — nothing stops you from calling &lt;code&gt;notifyListeners&lt;/code&gt; from a widget, and nothing stops you from reaching into another part of the tree with a god-object provider. Dependency ordering between providers is implicit and can get genuinely confusing at scale. And testing means either mounting widgets or mocking providers, which is more ceremony than the two alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Score: learning curve 5/5, boilerplate 5/5, testability 3/5, ecosystem 4/5, refactor cost 3/5, team fit 4/5.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Riverpod — The Same Idea, Grown Up
&lt;/h2&gt;

&lt;p&gt;Riverpod is what Provider became when the author took the lessons learned from the original and started over. Same mental model — read a value from the tree, get a rebuild when it changes — but with the weak spots engineered out. Providers are now top-level functions, which means they can be created, composed, and tested &lt;em&gt;without a widget tree at all&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;cartProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NotifierProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CartNotifier&lt;/span&gt;&lt;span class="p"&gt;,&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;CartItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;CartNotifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartNotifier&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Notifier&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;CartItem&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="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CartItem&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CartItem&lt;/span&gt; &lt;span class="n"&gt;item&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;state&lt;/span&gt; &lt;span class="o"&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;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// in a widget&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;cart&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;cartProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gains over Provider are measurable. Testability is dramatically better because you can build a &lt;code&gt;ProviderContainer&lt;/code&gt; in a plain Dart test and exercise providers directly. Dependency injection between providers is explicit — a provider can declare its dependencies by reading another provider, and the compiler catches mismatches. The DevTools integration (via the Riverpod extension) is excellent, with a state inspector that shows you the whole provider graph live.&lt;/p&gt;

&lt;p&gt;The costs: Riverpod adds a new vocabulary — &lt;code&gt;Notifier&lt;/code&gt;, &lt;code&gt;StateProvider&lt;/code&gt;, &lt;code&gt;FutureProvider&lt;/code&gt;, &lt;code&gt;StreamProvider&lt;/code&gt;, &lt;code&gt;ref.watch&lt;/code&gt; vs &lt;code&gt;ref.listen&lt;/code&gt; — and that vocabulary is the real learning curve. It is not that any one concept is hard; it is that there are many concepts, and beginners reach for the wrong provider type constantly. The refactoring cost also bites: because Riverpod is more opinionated, migrating a codebase to it is closer to a rewrite than a rename. If you learn it first, great. If you are coming from Provider, the migration of a large app is a real project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Score: learning curve 3/5, boilerplate 4/5, testability 5/5, ecosystem 4/5, refactor cost 3/5, team fit 4/5.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bloc — Structure With Teeth
&lt;/h2&gt;

&lt;p&gt;Bloc is the most opinionated of the three, and it wears that as a badge. The idea: state lives in &lt;code&gt;Bloc&lt;/code&gt; classes that receive &lt;code&gt;Event&lt;/code&gt;s and emit &lt;code&gt;State&lt;/code&gt;s, strictly one way. UI sends events; the bloc processes them and emits a new state; the UI rebuilds off the state. No widgets ever mutate state, and no state ever mutates widgets.&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;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddItem&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CartEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&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;item&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;CartItem&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartState&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartLoaded&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CartState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;CartLoaded&lt;/span&gt;&lt;span class="p"&gt;(&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;items&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CartItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartBloc&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Bloc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CartEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CartState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;CartBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&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;CartLoaded&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emit&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;CartLoaded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CartLoaded&lt;/span&gt;&lt;span class="p"&gt;([..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where Bloc wins is scale and team discipline. The explicit &lt;code&gt;Event → State&lt;/code&gt; contract makes flows auditable: you can read a bloc and see exactly what every user action can do. It is the easiest of the three to test in isolation — a bloc is plain Dart, no widget tree, so unit tests are straightforward and the &lt;code&gt;bloc_test&lt;/code&gt; package makes them nearly declarative. For a team of ten shipping a complex app, the ceremony is a feature, not a tax: it keeps everyone writing the same shape of code.&lt;/p&gt;

&lt;p&gt;Where Bloc costs you is up front. The boilerplate is real — events, states, and a bloc class for every feature, plus generated code if you go all in on the builder packages. Beginners drown in it, because the pattern has to be internalized before the code becomes readable. Debugging also has a rhythm you have to learn: you do not just read variables, you read event/state transitions in the bloc DevTools extension. It is the steepest curve of the three, and it only pays off if your app is complex enough to need the guardrails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Score: learning curve 2/5, boilerplate 2/5, testability 5/5, ecosystem 5/5, refactor cost 3/5, team fit 3/5.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Riverpod&lt;/th&gt;
&lt;th&gt;Bloc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate (less is better)&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem &amp;amp; tooling&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor cost (higher is worse)&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team fit at scale&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of them is broken. What the table shows is that they optimize for different stages of the same journey, and that is the real insight.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like in a Career
&lt;/h2&gt;

&lt;p&gt;Here is the pattern I have watched play out with dozens of developers, including myself. You learn Provider first because it is the smallest thing that works. You ship an app or three, and you hit the wall where the app is big enough that Provider's lack of structure makes the codebase hard to change without breaking things. Somewhere in there you discover Riverpod, and it feels like Provider with the training wheels off and the safety rails on — same mental model, real structure, and you can test it without a widget tree.&lt;/p&gt;

&lt;p&gt;Bloc is the destination for the people who need the discipline, the ones who land on large teams or complex products where consistency across ten developers matters more than how fast one developer can type. If you spend your career building solo apps, you may never need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Rule
&lt;/h2&gt;

&lt;p&gt;So, back to the question. Here is the framework I actually give people, in order of priority:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If you have never shipped a Flutter app: learn Provider first.&lt;/strong&gt; It is the smallest possible surface. Build two or three real apps with it. The concepts — inherited widgets, rebuilding on change, scoping — carry over to everything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you are building a serious app that you will maintain for a year or more, solo or with one or two people: use Riverpod.&lt;/strong&gt; It gives you testability and composition at a fraction of Bloc's ceremony, and it is where the ecosystem's energy is going.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you are on a team, or building something with genuinely complex flow (auth, onboarding, multi-role permissions): use Bloc.&lt;/strong&gt; The structure is the point. Let the boilerplate be the price of ten people agreeing on one shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never start a project with Bloc because it is "more powerful."&lt;/strong&gt; Power you do not need is just tax. Start small, and let the complexity of your app, not the hype of a package, move you up the ladder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one rule that outranks all of them: &lt;strong&gt;whatever you pick, do not spread state management across two solutions in the same app.&lt;/strong&gt; I have inherited codebases that mixed Provider and Bloc because a team "was migrating." That is the worst option on this list — worse than any of the three done consistently. Pick one, use it everywhere, and spend your time on features.&lt;/p&gt;

&lt;p&gt;The good news is that this is a two-week decision, not a two-year one. The concepts transfer. Learn Provider, ship something, and by the time you genuinely need more structure, you will know it — because your own codebase will tell you, the same way mine told me.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
    <item>
      <title>LLM Observability: Tracing, Logging, Debugging Agent Runs</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/llm-observability-tracing-logging-debugging-agent-runs-1je8</link>
      <guid>https://dev.to/mryadavgulshan/llm-observability-tracing-logging-debugging-agent-runs-1je8</guid>
      <description>&lt;p&gt;&lt;em&gt;Why your LLM app will fail silently, and how to see it before your customers do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three weeks. That is how long a customer-support agent shipped confidently wrong answers for a logistics client I work with before anyone noticed. The agent's job was simple: look up a shipment's status and reply to the customer. It did this hundreds of times a day. Every request returned HTTP 200. Latency was fine. The API bill looked normal. And the agent was quietly hallucinating tracking numbers.&lt;/p&gt;

&lt;p&gt;The first clue came from a phone call. A customer in Jeddah had been told her package was "delivered" — it was not. We pulled the logs. There were logs. They said: request received, model called, response returned, 200 OK. Nothing else. No record of what the retrieval step actually returned, no record of what the prompt looked like that day, no record of which model version answered, no record of how many tokens it burned to be wrong.&lt;/p&gt;

&lt;p&gt;That is the moment I stopped thinking about LLM observability as a nice-to-have and started treating it as the difference between a working system and an expensive black box. In this article I am going to walk you through everything I now do — and you should too — to trace, log, and debug LLM and agent runs in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLM Observability Is Not Regular Observability
&lt;/h2&gt;

&lt;p&gt;Traditional observability assumes your code is deterministic. You log an error, you see the stack trace, you find the bug, you fix it. An LLM breaks that assumption in four specific ways, and each one changes what "observable" means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The output is nondeterministic.&lt;/strong&gt; The same prompt can produce a correct answer at 9:00 AM and a hallucination at 9:15 AM with no code change in between. The system did not crash; it drifted. Your logs have to capture &lt;em&gt;state&lt;/em&gt;, not just errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hidden state is the whole story.&lt;/strong&gt; For a normal API you care about the request and response. For an LLM you also care about what was &lt;em&gt;inside&lt;/em&gt; the prompt — the retrieved chunks, the tool outputs, the system instructions, the model version. That is where wrongness hides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is per-token, and it compounds.&lt;/strong&gt; A retry loop, a bloated context, or a greedy agent can quietly multiply your bill 10x without throwing a single exception. You need token counts, not just request counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure is often fluent.&lt;/strong&gt; LLMs fail with perfect grammar and total confidence. Nobody files a bug report for a plausible wrong answer. Your logs are the only witness.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why "we log everything with our regular logging library" is not enough. You need tracing that reconstructs the &lt;em&gt;chain of decisions&lt;/em&gt;, not a flat list of calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Pillars: Tracing, Logging, and Evals
&lt;/h2&gt;

&lt;p&gt;When I design observability for an LLM system now, I build three layers, and they answer three different questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracing&lt;/strong&gt; answers "what happened, in what order, and how long did each step take?" A trace is a tree of spans. The root span is the request; child spans are retrieval, prompt assembly, each LLM call, each tool call. Every span carries duration, token counts, cost, model name, and the input/output that passed through it. This is what reconstructs the Jeddah incident — you can see the retrieval returned an empty chunk, the prompt went out truncated, and the model answered anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging&lt;/strong&gt; answers "what does this record look like for later analysis?" I log the full prompt and completion for every run, the retrieved chunks with their scores and sources, tool arguments and results, and a stable run ID. This is the raw material for audits, for compliance, for reproducing a specific failure after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evals and metrics&lt;/strong&gt; answer "is this getting better or worse?" Metrics are counters and gauges: tokens per request, latency percentiles, cost per resolved task, tool-call rate, cache hit rate. Evals are the scored test cases you run against a regression set when you change the prompt or the model. Tracing tells you what broke; evals tell you whether your fix stuck.&lt;/p&gt;

&lt;p&gt;The eval loop is where most teams quietly drop the ball, so let me be concrete about what it looks like. I keep a regression set of 50–100 real, de-identified interactions — a few per failure mode the team has hit. When I change a prompt, a retrieval strategy, or a model version, I run the set and score it with a mix of exact checks (the answer contains the correct tracking number) and rubric-based checks (did the agent escalate when the tool returned an error). The output is a pass rate and a diff against the previous run. If the change improves the reported incidents but drops the eval score by three points, I do not ship it. That eval set is the only reason I can move fast on prompts without being scared — the trace tells me &lt;em&gt;what&lt;/em&gt; changed, and the eval tells me &lt;em&gt;whether it is okay&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a Trace
&lt;/h2&gt;

&lt;p&gt;Let me show you what a real agent trace looks like. Take a question like "What is the status of order SH-991?" The root span splits into children, and each child carries its own numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request (root span)                          duration 3.1s   cost $0.084
├── auth + routing                           0.4ms           -
├── retrieval (vector store)                 28ms            top_k=5
│     └── 1 chunk returned (score 0.71)                      731 tokens
├── prompt assembly                          2ms             prompt=2,104 tokens
├── llm call 1                               1.9s            input 2,835 / output 142
│     └── decision: call_tool(lookup_tracking)
├── tool: lookup_tracking("SH-991")          120ms           400 error: not found
├── llm call 2                               860ms           input 3,112 / output 89
└── final answer                                              3,412 tokens total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one trace told me exactly what went wrong in the Jeddah case: retrieval returned a single low-confidence chunk, the tracking tool returned a hard error, and the model answered anyway instead of asking for a corrected reference number. Three spans, three failure points, one trace. Without it I would have spent days guessing.&lt;/p&gt;

&lt;p&gt;The ecosystem has settled on this shape. OpenTelemetry defined semantic conventions for generative AI (the &lt;code&gt;gen_ai.*&lt;/code&gt; attribute family — &lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;, &lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;) so traces from different providers and frameworks can be correlated in one tool. On top of the OTLP transport, you get purpose-built backends — I have used LangSmith and Langfuse, and both will happily ingest OTLP traces now. You do not need to pick between a tracing backend and an APM; they speak the same wire format.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Capture Per Run
&lt;/h2&gt;

&lt;p&gt;Here is the concrete capture contract I use. If a run produces these fields, I can debug any incident in under an hour:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;run_id&lt;/strong&gt; — stable across retries, correlatable to the customer session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;model&lt;/strong&gt; — provider, model name, and version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;temperature / top_p / max_tokens&lt;/strong&gt; — the sampling config at the time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;system prompt&lt;/strong&gt; — the full text, version-hashed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;retrieved chunks&lt;/strong&gt; — text, source, and score for each&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tool calls&lt;/strong&gt; — function name, serialized arguments, result, error if any&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;token counts&lt;/strong&gt; — prompt, completion, and total per call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;latency&lt;/strong&gt; — per span and total&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cost&lt;/strong&gt; — cents per call, from the token counts × current price&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;feedback&lt;/strong&gt; — thumbs up/down or a follow-up human rating when available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I write this to a Postgres table with a JSONB column for the trace plus a few indexed columns for querying, and I ship the same data to a tracing backend for the visual waterfall view. The table is the audit trail; the backend is the debugger.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal Instrumented Loop
&lt;/h2&gt;

&lt;p&gt;Here is a minimal agent loop with tracing bolted on — no framework, just the OpenTelemetry SDK and a couple of spans. This is the smallest thing I would actually deploy:&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;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;

&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.observability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;goal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;step.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;step_span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ret_span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;ret_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks.count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;ret_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks.scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm.call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;llm_span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;llm_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&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="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;llm_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;llm_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_final&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;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool.call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tool_span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;tool_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;tool_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool.arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**decision.arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;tool_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool.result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;step budget exhausted&lt;/span&gt;&lt;span class="sh"&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 key habit: every span carries the &lt;em&gt;input and output that moved through it&lt;/em&gt;. A span that only records duration is a pretty waterfall with no forensic value. When the trace shows &lt;code&gt;chunks.scores = [0.71]&lt;/code&gt; and the tool result is a hard error, the debugging is over before it starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Learned Debugging Real Runs
&lt;/h2&gt;

&lt;p&gt;Once tracing was live, the failure modes stopped being mysterious and became a checklist. These are the ones I hit, in order of frequency:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval returned nothing, and the model guessed.&lt;/strong&gt; The most common silent killer. The fix is never in the prompt; it is in the retrieval quality — better chunking, higher top-k, a fallback query. The trace proves it in one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context bloat.&lt;/strong&gt; A run that should use 4,000 tokens was sending 28,000 because history was never summarized. Latency doubled, cost quadrupled, and quality went &lt;em&gt;down&lt;/em&gt;. Token counts per span expose this instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool arguments out of schema.&lt;/strong&gt; The model invented a field or formatted a date wrong. With arguments logged, you can add a validation layer or tighten the tool description in one iteration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model drift after a provider update.&lt;/strong&gt; A silent provider-side change degraded one customer segment. Because I had &lt;code&gt;model version&lt;/code&gt; on every span, I could see the version flip exactly when accuracy dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost spikes tied to loops.&lt;/strong&gt; One agent was retrying the same failing tool call five times per conversation, doubling the bill. Step budgets are not a guardrail question; they are an observability question — the trace showed the loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I keep a running rule now: &lt;strong&gt;if a production incident takes more than an hour to explain, the tracing is insufficient.&lt;/strong&gt; Not the model, not the prompt — the tracing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Do NOT Need Full Observability
&lt;/h2&gt;

&lt;p&gt;Honest section, because not everything needs a tracing stack. If your use case is a stateless, single-shot LLM call — a summarizer, a classifier, a translation step with no tools and no retrieval — then a plain log of prompt, completion, model, and token count is 90% of the value at 10% of the setup cost. Ship that first.&lt;/p&gt;

&lt;p&gt;Similarly, if you are prototyping and have fewer than a few hundred calls a day, the tracing backend is overkill. Log to a JSON file. Add the full stack when you hit real users, real money, or a multi-step agent loop. The rule of thumb: &lt;strong&gt;one LLM call, no tools, no state → simple logging. A loop, tools, retrieval, or autonomy → tracing, non-negotiable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is also a spectrum between the two extremes, and most teams are on it. If you are at the "loop with tools" stage but not yet at "needs alerting," start with the logging contract and the eval set, and defer the backend until the trace viewer is actually going to save you time. The mistake I see is the reverse: teams buy the expensive backend first and never build the logging contract, so they have a beautiful waterfall view of runs that do not contain the one field that would explain the incident. The data comes first. The tool is the last mile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerting: Making the Trace Do Work
&lt;/h2&gt;

&lt;p&gt;The final layer is alerting, and it is the one that turns observability from a postmortem tool into a prevention tool. I set alerts on the metrics that predict incidents before customers feel them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost per resolved task&lt;/strong&gt; over a rolling day — when it spikes 30% above baseline, a loop or a context bloat is likely eating money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool-call rate per run&lt;/strong&gt; — when an agent that should call a tool in 80% of runs suddenly calls it in 30%, it has started guessing from training data. That is the "silent degradation" failure mode, caught in hours instead of weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rate on tool executions&lt;/strong&gt; — a spike means a downstream API broke, and the agent is about to hallucinate around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average retrieval score&lt;/strong&gt; — a creeping drop means the chunking or the embeddings drifted, and every answer will get worse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alert thresholds have to be tuned to each system, but the principle is universal: alert on &lt;em&gt;drift from the system's own baseline&lt;/em&gt;, not on absolute numbers. A 500-millisecond latency spike means nothing for a batch summarizer and everything for a chat product. Your historical traces are the baseline; the alert just detects the divergence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practitioner's Checklist
&lt;/h2&gt;

&lt;p&gt;Before you call an LLM system production-ready, go through this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every run has a stable &lt;code&gt;run_id&lt;/code&gt; correlated to the user session&lt;/li&gt;
&lt;li&gt;[ ] Prompt and completion are logged verbatim, with model name and version&lt;/li&gt;
&lt;li&gt;[ ] Retrieved chunks carry source and score&lt;/li&gt;
&lt;li&gt;[ ] Tool calls log arguments, result, and error&lt;/li&gt;
&lt;li&gt;[ ] Token counts (input/output) and cost are recorded per call&lt;/li&gt;
&lt;li&gt;[ ] Latency is measured per span, not just end-to-end&lt;/li&gt;
&lt;li&gt;[ ] A trace viewer can reconstruct any run in under five minutes&lt;/li&gt;
&lt;li&gt;[ ] A regression eval set exists, and it runs on every prompt change&lt;/li&gt;
&lt;li&gt;[ ] Alerts fire on cost-per-task, tool-call rate, and error-rate thresholds&lt;/li&gt;
&lt;li&gt;[ ] You can answer "what changed" for any incident — model, prompt, or data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Aftermath
&lt;/h2&gt;

&lt;p&gt;After the Jeddah incident we rebuilt the agent's tracing from scratch, added a guard that asks the customer to re-confirm the reference number when retrieval returns nothing, and put the whole thing behind the observability stack I described. Two months later, the client asked what the new dashboards cost. When I told him, he laughed and said the alternative — three more weeks of confident wrong answers — would have cost his dispatch team more than that in a single day.&lt;/p&gt;

&lt;p&gt;Your LLM will fail silently. It is not a question of whether; it is a question of how long you do not know about it. Tracing, logging, and evals are the difference between finding out in hours and finding out from an angry customer.&lt;/p&gt;

&lt;p&gt;If you are starting today, do not buy a tool. Add the run ID, log the prompt and completion, and put token counts on every call. That is the whole foundation. Everything else — the backends, the dashboards, the alerting — is polish on top of a habit you have to build first.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Flutter State Management: Riverpod vs Bloc in 2026</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Sat, 29 Aug 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/flutter-state-management-riverpod-vs-bloc-in-2026-4aj2</link>
      <guid>https://dev.to/mryadavgulshan/flutter-state-management-riverpod-vs-bloc-in-2026-4aj2</guid>
      <description>&lt;p&gt;&lt;em&gt;Two state management libraries, real production apps behind both. A criteria table, honest scores, and a decision rule you can apply to your own project.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every Flutter developer reaches the same junction within their first month: Riverpod or Bloc. I have been asked this question in some form for years, and I have shipped both — Riverpod in a consumer finance app, Bloc in a large enterprise product with multiple teams touching the same codebase. Both work. Both have passionate defenders. And the answer, as with most engineering questions, depends entirely on your context.&lt;/p&gt;

&lt;p&gt;This article is that question answered honestly: a criteria table, each tool scored on the dimensions that actually matter when real teams build real apps, and a decision rule at the end. I am not going to tell you one is universally better, because that would be a lie you would discover in week three.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Criteria That Actually Matter
&lt;/h2&gt;

&lt;p&gt;Marketing material for both libraries sounds identical. These are the criteria I have learned to weight, from shipping both in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Learning curve.&lt;/strong&gt; How long before a competent developer writes idiomatic code, not copy-pasted code?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boilerplate and ceremony.&lt;/strong&gt; How many lines does a simple feature actually take?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async handling.&lt;/strong&gt; How well does it model loading, success, and error states — which is most of a real app?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability.&lt;/strong&gt; How easy is it to test business logic in isolation from widgets?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev tools and debugging.&lt;/strong&gt; When state is wrong, how fast can you find it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability and team ergonomics.&lt;/strong&gt; How does it behave when 8 developers work in the same repo?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem and maintenance.&lt;/strong&gt; Is it alive, documented, and surrounded by answers?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Criteria Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Riverpod&lt;/th&gt;
&lt;th&gt;Bloc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;8/10 — gradual, small concepts build up&lt;/td&gt;
&lt;td&gt;6/10 — events/emitters/states take a mental shift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate&lt;/td&gt;
&lt;td&gt;7/10 — lean for simple cases, still explicit&lt;/td&gt;
&lt;td&gt;5/10 — more files and classes per feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async handling&lt;/td&gt;
&lt;td&gt;9/10 — AsyncValue is a first-class citizen&lt;/td&gt;
&lt;td&gt;8/10 — state + sealed classes, manual but clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;8/10 — providers are easy to override&lt;/td&gt;
&lt;td&gt;9/10 — pure event-to-state, the classic winner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev tools&lt;/td&gt;
&lt;td&gt;7/10 — growing, decent&lt;/td&gt;
&lt;td&gt;9/10 — Bloc Inspector is excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team ergonomics&lt;/td&gt;
&lt;td&gt;7/10 — flexibility invites inconsistent patterns&lt;/td&gt;
&lt;td&gt;9/10 — enforced structure scales to many devs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;8/10 — active, mainstream&lt;/td&gt;
&lt;td&gt;9/10 — the most battle-tested large codebase story&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No tool wins every row, and that is the point — the correct choice is a function of your team, your app, and your tolerance for structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Riverpod — Score: 7.5/10, the Flexible Modern Default
&lt;/h2&gt;

&lt;p&gt;Riverpod is the direct descendant of Provider, written to fix Provider's flaws: no compile-time safety, no easy testing, no way to combine providers. It rethinks state management around a single concept — the provider — and gives you compile-time safety, easy overrides for testing, and async state handled natively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning curve: 8/10.&lt;/strong&gt; The mental model is small: a provider is a piece of state that knows how to recreate itself. A simple counter takes one provider and a few lines:&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;counterProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StateProvider&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="n"&gt;ref&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CounterView&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ConsumerWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WidgetRef&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;count&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;counterProvider&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;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;FilledButton&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;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;counterProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Count: &lt;/span&gt;&lt;span class="si"&gt;$count&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;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole pattern. &lt;code&gt;ref.watch&lt;/code&gt; rebuilds the widget when state changes, &lt;code&gt;ref.read&lt;/code&gt; reads without subscribing. New developers pick this up in an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async handling: 9/10.&lt;/strong&gt; This is Riverpod's best feature. &lt;code&gt;AsyncValue&lt;/code&gt; models loading, data, and error as a single sealed type, so async state stops being an ad-hoc boolean-and-null dance:&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;userProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FutureProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// in a widget:&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;userAsync&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;userProvider&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;userAsync&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;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Error: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;'&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;user&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;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No separate loading flags, no null checks, no forgotten error states. Every consumer of the provider gets correct async handling by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testability: 8/10.&lt;/strong&gt; Providers are overridable, which makes testing straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WidgetRef&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ProviderScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;overrides:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;userProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;overrideWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncValue&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="n"&gt;fakeUser&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;
    &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&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;You swap a real provider for a fake in one line. It is clean and it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest weak spots.&lt;/strong&gt; Riverpod gives you freedom, and freedom means inconsistency. In a codebase with several developers, I have seen the same feature implemented three different ways because Riverpod offers several valid approaches — &lt;code&gt;StateProvider&lt;/code&gt;, &lt;code&gt;Notifier&lt;/code&gt;, &lt;code&gt;AsyncNotifier&lt;/code&gt;, &lt;code&gt;StreamProvider&lt;/code&gt; — and nothing forces a team to agree on one. The Bloc Inspector also has no true rival: Riverpod's tooling is decent but does not give you the same time-travel-style event timeline. And the &lt;code&gt;ConsumerWidget&lt;/code&gt; / &lt;code&gt;ConsumerStatefulWidget&lt;/code&gt; split confuses newcomers until they internalize it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest verdict:&lt;/strong&gt; Riverpod is the correct default for most apps and most teams — small mental model, superb async handling, easy testing, and fast to write. Choose it when you value developer velocity, when the team is small, or when you are building greenfield and want to move quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bloc — Score: 8/10, the Structured Heavyweight
&lt;/h2&gt;

&lt;p&gt;Bloc takes the opposite philosophy. State is an immutable value, and the only way to change it is to send an event through a bloc that maps events to states. Everything is explicit, everything is typed, and everything is testable by construction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning curve: 6/10.&lt;/strong&gt; The concepts are simple — event in, state out — but the ceremony is real. The same counter requires more files and a mental shift from "widget owns state" to "bloc owns 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="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CounterEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CounterIncremented&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CounterEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CounterBloc&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Bloc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CounterEvent&lt;/span&gt;&lt;span class="p"&gt;,&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="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;CounterBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CounterIncremented&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emit&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;emit&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="mi"&gt;1&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;And to use it in a widget you add a &lt;code&gt;BlocProvider&lt;/code&gt; and watch with &lt;code&gt;context.watch&amp;lt;CounterBloc&amp;gt;()&lt;/code&gt;. Every feature means an event class, a bloc class, and a state definition — usually spread across files. The first week is slower, and teams new to Bloc feel it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async handling: 8/10.&lt;/strong&gt; Bloc handles async explicitly. You emit a loading state, do the work, and emit a success or failure state — usually with sealed classes so every state is exhaustive:&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;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserState&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserLoading&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserState&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserLoaded&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserState&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;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;UserLoaded&lt;/span&gt;&lt;span class="p"&gt;(&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;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserState&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;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;(&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;message&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 is more code than &lt;code&gt;AsyncValue&lt;/code&gt;, and you write the state machine yourself — but you control it completely, and nothing is hidden from you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testability: 9/10.&lt;/strong&gt; This is Bloc's crown. A bloc is pure logic: events in, states out, no widgets involved. Testing is as clean as it gets:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'increments the counter'&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="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;bloc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CounterBloc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;bloc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CounterIncremented&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bloc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&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;No widget tree, no mocking framework, no async pump. Pure function-of-input-to-output testing, which is why enterprise teams love it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev tools: 9/10.&lt;/strong&gt; The Bloc Inspector is genuinely excellent — you can watch every event and every state transition in a timeline, which turns debugging a bad state into a searchable history instead of a guessing game. It is the single best debugging experience in Flutter state management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest weak spots.&lt;/strong&gt; The boilerplate is real and grows with complexity. For a simple feature, Bloc is objectively more work than Riverpod. The strictness that helps a large team also slows a solo developer down, and junior developers produce Bloc code that is correct but bloated — one bloc class per screen, events that do one thing and emit three states. And because the structure is so explicit, refactoring a Bloc architecture touches more files than the equivalent Riverpod change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest verdict:&lt;/strong&gt; Bloc is the right choice when your app is large, your team is multiple developers, or your state logic is complex enough that enforced structure saves more time than it costs. It is the library that scales — not to more code, but to more people and more years of maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real-World Decision Rules
&lt;/h2&gt;

&lt;p&gt;Here is the decision rule I now apply with clients, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Small app, small team, greenfield, want to ship fast? Use Riverpod.&lt;/strong&gt; The learning curve is lower, the code is shorter, and &lt;code&gt;AsyncValue&lt;/code&gt; removes a whole class of async bugs for free. You can rebuild with structure later if you need to — and if you started on Riverpod, your feature code is mostly presentation logic, so the migration cost is contained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multiple developers, long-lived codebase, complex state flows? Use Bloc.&lt;/strong&gt; The enforced event-to-state structure is not overhead; it is the communication protocol your team uses to avoid stepping on each other. The Inspector and the pure testability more than pay for the ceremony. On a codebase with 6+ people, I choose Bloc every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Already have a codebase in one of them? Do not switch for fashion.&lt;/strong&gt; A migration of a working app costs weeks and produces zero user-facing value. I have told teams this directly: your problem is not the library. Unless a concrete pain — async bugs, unmanageable boilerplate, impossible testing — is attributable to the choice, keep what works and improve the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Mixed seniority on the team? Match the structure to the weakest link.&lt;/strong&gt; If your team is mostly seniors, Riverpod's freedom is a feature. If you onboard juniors constantly, Bloc's rigidity trains them to write consistent code. The library is, in part, a training mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Worked Example: Scoring Your Own Situation
&lt;/h2&gt;

&lt;p&gt;Let me apply the rule to the two projects I mentioned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The consumer finance app (Riverpod).&lt;/strong&gt; Small team, three developers, heavy async — live balances, transaction history, push notifications. The &lt;code&gt;AsyncValue&lt;/code&gt; model alone removed dozens of "forgot to handle the error state" bugs. We shipped features in days, not weeks. Bloc would have been slower with zero benefit at this team size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The enterprise product (Bloc).&lt;/strong&gt; Eight developers across three teams touching the same modules, a codebase that will be maintained for years, and strict QA requirements. The event-to-state pattern gave every team a uniform way to read any screen's logic, and the Inspector made support escalations ten times faster. Riverpod's flexibility would have produced five inconsistent patterns in a codebase that large.&lt;/p&gt;

&lt;p&gt;Same framework, two opposite answers, both correct — because the decision was driven by team size and codebase lifetime, not by which library was "better."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers That Decide It
&lt;/h2&gt;

&lt;p&gt;The real trade-off, compressed into the numbers that matter: &lt;strong&gt;development speed&lt;/strong&gt; (Riverpod wins for small teams), &lt;strong&gt;debugging speed&lt;/strong&gt; (Bloc wins via the Inspector), &lt;strong&gt;test maintenance&lt;/strong&gt; (Bloc's pure logic tests are cheaper to keep green), and &lt;strong&gt;codebase consistency&lt;/strong&gt; (Bloc forces it, Riverpod requires discipline). A solo developer or a three-person startup is optimizing the first number. A 50-developer org is optimizing the last three.&lt;/p&gt;

&lt;p&gt;The decision rule, one sentence: &lt;strong&gt;Riverpod for velocity and async ergonomics on small teams; Bloc for structure, testability, and multi-developer scale.&lt;/strong&gt; Score your own situation honestly — team size, app complexity, how long the codebase will live — and the choice stops being a flame war and becomes a calculation.&lt;/p&gt;

&lt;p&gt;I have shipped both in production apps with real users and real bugs. Riverpod made me faster; Bloc made my codebase easier to reason about at scale. The best tool is the one that matches the size and shape of the problem you actually have — so measure your team, not your preference.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
    <item>
      <title>RAG Evaluation: How to Know Your Retrieval Isn't Broken</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Sat, 29 Aug 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/rag-evaluation-how-to-know-your-retrieval-isnt-broken-5d81</link>
      <guid>https://dev.to/mryadavgulshan/rag-evaluation-how-to-know-your-retrieval-isnt-broken-5d81</guid>
      <description>&lt;p&gt;&lt;em&gt;The metric taxonomy for RAG — retrieval metrics, generation metrics, the eval sets you build, and the numbers that tell you which layer is failing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I was reviewing a team's RAG demo at a meetup last year. The presenter asked a question, the system retrieved a chunk, and the LLM produced a fluent, well-cited answer. It looked perfect. I asked the question nobody in the room could answer: "What is your hit rate?" The presenter blinked. "What do you mean, hit rate?" We checked their logs. The retrieval was returning the correct chunk about 30% of the time — and because the demo questions were chosen for answers the system could find, nobody had noticed.&lt;/p&gt;

&lt;p&gt;That is the whole story of RAG evaluation in one incident: fluent output hides broken retrieval. The LLM is a confident text generator, so a wrong chunk in the context produces an answer that sounds exactly as good as a right one. The only way to know which is which is to measure the layers independently, with metrics, on data you control.&lt;/p&gt;

&lt;p&gt;I have written at length about why retrieval quality matters more than model choice. This article is the how: the metric taxonomy, the eval sets you build, the tooling that runs the numbers, and the honest rules for when this evaluation is worth the effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Cannot Just "Look at Answers"
&lt;/h2&gt;

&lt;p&gt;The trap is that humans are terrible at evaluating RAG output by eyeballing ten examples. Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fluency is not correctness.&lt;/strong&gt; The model writes with equal confidence whether the retrieved chunk is right or wrong. A human reader cannot distinguish "grounded in the right source" from "plausible-sounding" without checking the source themselves — and nobody does that for every answer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You cannot see the failure that did not happen.&lt;/strong&gt; A demo question that happens to retrieve well tells you nothing about the 90% of real questions that do not. The only way to see the failure distribution is to run a fixed, labelled set repeatedly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The failure lives in the retrieval layer, not the output.&lt;/strong&gt; If retrieval returns the wrong chunk, no amount of prompt or model tuning fixes the answer. To know where to invest, you must be able to score retrieval and generation separately. A single "did the answer look good" number tells you nothing about where to fix things.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the entire practice of RAG evaluation reduces to one question, asked with data: which layer is losing quality, retrieval or generation?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Metric Taxonomy
&lt;/h2&gt;

&lt;p&gt;RAG evaluation splits into two families, and the biggest mistake I see is teams measuring one family and calling it done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieval Metrics — "Did we find the right source?"
&lt;/h3&gt;

&lt;p&gt;These score the retriever alone, before the LLM ever sees the context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hit rate / Recall@k.&lt;/strong&gt; Out of all queries, what fraction had the correct document in the top-k retrieved results? This is the single most important number in all of RAG. If your hit rate at k=5 is below 80%, your retriever is the problem, full stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MRR (Mean Reciprocal Rank).&lt;/strong&gt; Where in the ranked list did the correct document appear? A system that always returns the right chunk at position 1 has MRR 1.0; one that buries it at position 4 is worse even if it still "hits." Latency and context budget reward high ranks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precision@k.&lt;/strong&gt; Of the k retrieved chunks, what fraction were relevant? This matters because irrelevant chunks cost tokens and confuse the model. A system that hits at position 5 but returns four noise chunks above it will generate worse answers than one that hits cleanly at position 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NDCG.&lt;/strong&gt; The graded version — accounts for whether near-miss chunks are partially relevant. Overkill for most teams; MRR and hit rate cover 95% of decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Generation Metrics — "Did the answer use the source correctly?"
&lt;/h3&gt;

&lt;p&gt;These score the LLM's output against the retrieved context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faithfulness.&lt;/strong&gt; What fraction of the claims in the answer are supported by the retrieved context? This is the "is it grounded or hallucinating" number. It is the metric that would have caught my client's wrong-dosage disaster if they had run it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer relevancy.&lt;/strong&gt; Does the answer actually address the user's question? An answer can be perfectly faithful to the retrieved context and still not answer what was asked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context precision.&lt;/strong&gt; Of the retrieved chunks, how many were actually needed to produce the answer? High context precision means the retriever is giving the model clean, useful material.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context recall.&lt;/strong&gt; Could the answer have been produced from the retrieved chunks at all? This is generation-side view of the same hit-rate question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The diagnostic logic: &lt;strong&gt;if hit rate or context recall is low, fix retrieval. If faithfulness is low, your retrieval is often still fine — the problem is prompt handling, context stuffing, or the model.&lt;/strong&gt; These two families tell you exactly where to spend your engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Eval Set (The Part Everyone Skips)
&lt;/h2&gt;

&lt;p&gt;Metrics are meaningless without a labelled set. This is the highest-leverage work in the entire pipeline, and it is the part most teams skip because it is not glamorous. The rule I use: start with 100–200 question–document pairs built from your real logs, not from happy-path examples a developer wrote.&lt;/p&gt;

&lt;p&gt;How to build it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mine real queries.&lt;/strong&gt; Pull the last month of actual user questions from your logs. These are your ground truth for what the system faces, not what you imagine it faces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label the ground truth.&lt;/strong&gt; For each query, identify the document chunks that contain the correct answer. You are labelling the answer source, not the answer itself. This is tedious work, and it is exactly the work that makes every other number meaningful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include the hard cases.&lt;/strong&gt; Multi-hop questions, questions with exact codes or SKUs, questions that require combining two documents. If your eval set has no hard cases, your metrics will look great and your production will not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version the set.&lt;/strong&gt; It lives in your repo, in git. When you change the chunker, the embedding model, or the retriever, the same set tells you whether you got better or worse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The honest advice: label 50 examples well before you label 200 badly. A clean small set beats a noisy large one, and you can grow it every week by having the team review a handful of real queries each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Numbers with RAGAS
&lt;/h2&gt;

&lt;p&gt;You do not need to hand-score each answer. LLM-as-a-judge has matured enough that for faithfulness-type metrics it is reliable — the judge model scores whether claims are supported by context, which is a verification task, not a reasoning task. The library I reach for is RAGAS, which implements the retrieval and generation metrics above and scores a full eval set in one pass.&lt;/p&gt;

&lt;p&gt;Here is the setup. Install, define your eval set as question/answer/context triplets, and run:&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;ragas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;evaluate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;

&lt;span class="n"&gt;eval_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_list&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the refund window for annual plans?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Annual plans can be refunded within 14 days of purchase.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_contexts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Annual subscriptions are eligible for a full refund within 14 days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refund window for annual plans is 14 days.&lt;/span&gt;&lt;span class="sh"&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;# ... 100+ more rows from your real logs
&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;eval_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;judge_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# a strong LLM acting as the judge
&lt;/span&gt;    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;judge_embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result_df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_recall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_relevancy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few honest notes on the tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The judge model matters.&lt;/strong&gt; Use a strong model — it is a verification pass, and a weak judge produces noise that hides real regressions. If you are on a budget, judge with a strong hosted model and only self-host the judge once your evaluation volume justifies it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;reference&lt;/code&gt; field is your gold standard.&lt;/strong&gt; For retrieval metrics, RAGAS scores context recall by checking whether the reference is contained in the retrieved chunks. The quality of your labels directly sets the quality of every number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it in CI, not just on demand.&lt;/strong&gt; A weekly run that compares today's metrics to last week's catches regressions from chunking changes, embedding swaps, or retriever config edits. The eval set has no value sitting in a notebook that nobody re-runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Numbers Actually Tell You
&lt;/h2&gt;

&lt;p&gt;Reading the output is a diagnosis, not a score. Here is the decision table I work from:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What to fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hit rate low (&amp;lt;80% at k=5)&lt;/td&gt;
&lt;td&gt;Retriever not finding the source&lt;/td&gt;
&lt;td&gt;Chunking, embedding model, top-k, hybrid search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR low but hit rate okay&lt;/td&gt;
&lt;td&gt;Right doc is buried&lt;/td&gt;
&lt;td&gt;Re-ranking, larger top-k, better index params&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context precision low&lt;/td&gt;
&lt;td&gt;Retrieval returns noise around the right doc&lt;/td&gt;
&lt;td&gt;Tighter chunking, re-ranking, hybrid + fusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faithfulness low&lt;/td&gt;
&lt;td&gt;Answer not supported by context&lt;/td&gt;
&lt;td&gt;Context stuffing discipline, prompt hardening, model choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer relevancy low&lt;/td&gt;
&lt;td&gt;Not answering the question asked&lt;/td&gt;
&lt;td&gt;Query understanding, reformulation, better instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most common real-world pattern I see: teams with high faithfulness on the demo and low hit rate on the full set. That combination is a classic — retrieval returns irrelevant chunks for most real queries, and the LLM faithfully summarizes garbage. Fix retrieval first; the generation metrics will follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Online vs Offline Evaluation
&lt;/h2&gt;

&lt;p&gt;Offline evaluation on a labelled set catches regressions you control. But it cannot catch the two failures that only appear with real traffic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Query drift.&lt;/strong&gt; Users ask things you never predicted. The eval set grows stale, and the system quietly rots against the questions it actually faces. The fix is a feedback loop: instrument production, log every query and its retrieved chunks, and add samples to the eval set weekly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent degradation.&lt;/strong&gt; A retriever config change or an embedding model update ships, retrieval quality drops, and nobody notices because no alert exists. The fix is scheduled re-evaluation in CI plus a guardrail: a model or config change does not merge unless the eval-set metrics hold or improve.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For online observability, track two lightweight numbers in production: &lt;strong&gt;retrieval coverage&lt;/strong&gt; — the fraction of queries that return results above your relevance floor — and &lt;strong&gt;feedback signals&lt;/strong&gt; — thumbs up/down, copy clicks, whether the user rephrased the question. Correlated with your weekly offline run, these catch the drift the labelled set cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Over-Engineer Evaluation
&lt;/h2&gt;

&lt;p&gt;The honest boundary, because evaluation has a cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If your RAG is a weekend project&lt;/strong&gt; with a few dozen documents, hand-check answers and skip the harness. The effort is not worth it at that scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you have no labelled set and no willingness to build one&lt;/strong&gt;, do not buy metric tooling — you would be measuring noise. Start with 50 labelled pairs and grow from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your failure cost is low&lt;/strong&gt; — a demo, an internal tool nobody depends on — a light evaluation loop is enough. Reserve the full CI harness for systems where a wrong answer has a real cost, like a clinician-facing assistant or a legal research tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: evaluation effort should track the cost of being wrong. A wrong answer in a customer-facing finance assistant is worth a full harness; a wrong answer in your personal note-summarizer is worth a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practitioner's Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] You can answer "what is your hit rate at k=5" with a number, today&lt;/li&gt;
&lt;li&gt;[ ] Retrieval and generation are scored separately; a single blended number is not trusted&lt;/li&gt;
&lt;li&gt;[ ] Eval set has 100+ question–document pairs built from real logs, versioned in git&lt;/li&gt;
&lt;li&gt;[ ] The set includes hard cases: multi-hop, exact codes, multi-document questions&lt;/li&gt;
&lt;li&gt;[ ] Labels are reviewed by a human; the judge model only scores, it does not define truth&lt;/li&gt;
&lt;li&gt;[ ] Retrieval metrics: hit rate, MRR, precision@k — tracked over time&lt;/li&gt;
&lt;li&gt;[ ] Generation metrics: faithfulness, answer relevancy — tracked over time&lt;/li&gt;
&lt;li&gt;[ ] A diagnostic table maps low metrics to the layer to fix (retriever vs prompt vs model)&lt;/li&gt;
&lt;li&gt;[ ] Evaluation runs in CI on every model/config change; regressions block the merge&lt;/li&gt;
&lt;li&gt;[ ] Production logs feed the eval set weekly so it does not go stale&lt;/li&gt;
&lt;li&gt;[ ] You know when to stop — evaluation effort scales with the cost of being wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Number That Changed the Room
&lt;/h2&gt;

&lt;p&gt;That meetup demo ended with a different conversation than the presenter planned. We added hit rate to their monitoring, found the 30% number, and traced it to a chunking strategy that had never been tested against their real documents. Three weeks later retrieval was above 85%, and the fluent answers were finally grounded — not because the model changed, but because someone finally measured the layer underneath it.&lt;/p&gt;

&lt;p&gt;When someone tells you their RAG is working, ask for their hit rate. If they cannot answer, the system is not evaluated, it is demoed. Build the set, run the numbers, and let the metrics point at the layer that is actually leaking quality. That is the entire discipline, and it is the difference between a system that sounds grounded and one that is.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>machinelearning</category>
    </item>
    <item>
      <title>OpenTelemetry vs Prometheus: Why You Almost Certainly Need Both</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Fri, 28 Aug 2026 17:23:43 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/opentelemetry-vs-prometheus-why-you-almost-certainly-need-both-2b1a</link>
      <guid>https://dev.to/mryadavgulshan/opentelemetry-vs-prometheus-why-you-almost-certainly-need-both-2b1a</guid>
      <description>&lt;p&gt;This gets framed as a choice, and it mostly isn't one. They occupy different layers, and a normal production setup runs both. Understanding why saves a lot of wasted migration effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each one actually is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prometheus is a metrics database with a query language.&lt;/strong&gt; It scrapes numeric time series from your services, stores them, and lets you query them with PromQL. It is a &lt;em&gt;backend&lt;/em&gt;: a place metrics live and are asked questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenTelemetry is a specification and a set of SDKs for producing telemetry.&lt;/strong&gt; It defines how you instrument code to emit traces, metrics and logs, and it ships a Collector that receives, processes and forwards them. It is a &lt;em&gt;pipeline&lt;/em&gt;: how telemetry gets from your code to wherever it is stored.&lt;/p&gt;

&lt;p&gt;OTel does not store anything long-term. Prometheus does not instrument your code. Asking which to use is a little like asking whether to use a courier or a warehouse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the overlap actually is
&lt;/h2&gt;

&lt;p&gt;There is one real point of contention: &lt;strong&gt;metrics instrumentation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Historically you added a Prometheus client library to your service, defined counters and histograms, and exposed &lt;code&gt;/metrics&lt;/code&gt; for Prometheus to scrape. That works and is still widely deployed.&lt;/p&gt;

&lt;p&gt;OpenTelemetry offers its own metrics SDK doing the same job, but vendor-neutrally — the same instrumentation can be exported to Prometheus, or to a commercial backend, or to several at once, without touching application code.&lt;/p&gt;

&lt;p&gt;So the genuine question is not "Prometheus or OTel" but &lt;strong&gt;"Prometheus client libraries or OTel SDKs, in front of Prometheus?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The answer for most teams
&lt;/h2&gt;

&lt;p&gt;Instrument with OpenTelemetry. Store metrics in Prometheus.&lt;/p&gt;

&lt;p&gt;The reason is not that OTel's metrics API is nicer — arguably the Prometheus client is simpler for pure metrics. It is that instrumentation is the expensive, sticky part. It is spread across every service, written by everyone, and rewriting it later is a large coordinated change nobody wants to schedule.&lt;/p&gt;

&lt;p&gt;Backends are comparatively easy to change. If your instrumentation is vendor-neutral, swapping or adding a backend is a Collector config change. If your instrumentation is Prometheus-specific, it is a code change in every service.&lt;/p&gt;

&lt;p&gt;Instrument once against a standard; keep the storage decision reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other reason: traces
&lt;/h2&gt;

&lt;p&gt;Prometheus stores metrics. Metrics tell you &lt;em&gt;that&lt;/em&gt; p99 latency rose. They cannot tell you &lt;em&gt;which&lt;/em&gt; call in the request path caused it, because a metric is an aggregate — the individual requests were summed away.&lt;/p&gt;

&lt;p&gt;Traces keep the individual request: every span, in order, with timing. When latency rises, a trace shows you it was a specific downstream call, on a specific code path, for a specific class of request.&lt;/p&gt;

&lt;p&gt;Prometheus has no trace story. OpenTelemetry treats traces as first-class and links them to metrics through exemplars — so a spike in a histogram can carry a pointer to an actual slow request. In practice that link is the single most useful thing in the stack, and you only get it if traces and metrics come from the same instrumentation.&lt;/p&gt;

&lt;p&gt;If you have only ever had metrics, this is the upgrade worth making. Most debugging time is spent going from "something is slow" to "this is what is slow", and that is precisely the gap traces close.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real setup looks like
&lt;/h2&gt;

&lt;p&gt;Application code instrumented with OTel SDKs, emitting traces and metrics. An OTel Collector deployed per host or as a service, receiving that telemetry. From the Collector: metrics to Prometheus, traces to Tempo or Jaeger, logs to Loki. Grafana on top, querying all three.&lt;/p&gt;

&lt;p&gt;The Collector is more useful than it first appears. Because everything passes through it, you can drop high-cardinality attributes, sample traces, redact fields that should never reach a vendor, and add resource attributes centrally. Doing any of that without a collection layer means redeploying every service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap worth naming
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cardinality.&lt;/strong&gt; Prometheus stores a separate time series for every unique combination of label values. Add a label with unbounded values — user ID, request ID, full URL path — and you generate millions of series. Prometheus will slow down and then fall over, and the failure looks like a Prometheus problem rather than an instrumentation problem.&lt;/p&gt;

&lt;p&gt;OpenTelemetry makes this &lt;em&gt;easier&lt;/em&gt; to do accidentally, because trace attributes are naturally high-cardinality and it is tempting to promote them to metric labels. Traces handle high cardinality fine. Metrics do not. Keep the distinction: identifying details belong on spans, and metric labels stay bounded and low.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you do not need OTel
&lt;/h2&gt;

&lt;p&gt;If you run a handful of services, you already have Prometheus client libraries working, and nobody is asking questions your metrics cannot answer — there is no urgent reason to migrate. It is real work for benefit you may not currently need.&lt;/p&gt;

&lt;p&gt;The trigger to adopt it is usually one of: you want distributed tracing; you are tired of debugging across service boundaries with only aggregates; or you want to stop being locked to one backend's instrumentation format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Not competitors, and not really a choice. OpenTelemetry is how telemetry is produced and moved; Prometheus is where metrics land and get queried. Instrument with OTel because instrumentation is the part you cannot cheaply redo, keep Prometheus as the metrics backend because it is excellent at that job, and add tracing because it answers the questions metrics structurally cannot.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.misar.blog/@mrgulshanyadav/articles/opentelemetry-vs-prometheus-why-you-almost-certainly-need-both?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;https://www.misar.blog/@mrgulshanyadav/articles/opentelemetry-vs-prometheus-why-you-almost-certainly-need-both&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>prometheus</category>
      <category>observability</category>
      <category>devops</category>
    </item>
    <item>
      <title>Downloading Files to the Device in Flutter</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Fri, 28 Aug 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/downloading-files-to-the-device-in-flutter-3b96</link>
      <guid>https://dev.to/mryadavgulshan/downloading-files-to-the-device-in-flutter-3b96</guid>
      <description>&lt;p&gt;So, in this article, I will be showing you how you can download files to the device in Flutter — and actually save them somewhere the user can find them.&lt;/p&gt;

&lt;p&gt;This sounds trivial until you ship it. I have debugged more "the download finished but the file is nowhere" reports than I can count, and the root cause is almost always the same: the code downloads bytes correctly and then writes them to a location the user has no way to reach, or Android's storage rules block the write silently. Every generation of Android changes the rules slightly, which is why a tutorial from two years ago will compile fine and then fail on a modern device.&lt;/p&gt;

&lt;p&gt;The good news: the pattern is stable, the packages are mature, and once you understand where each platform expects files to go, the whole thing is about thirty lines of Dart.&lt;/p&gt;

&lt;p&gt;For this purpose, we need to add these dependencies in your &lt;code&gt;pubspec.yaml&lt;/code&gt; file:&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;dio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^5.4.0&lt;/span&gt;
  &lt;span class="na"&gt;path_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^2.1.0&lt;/span&gt;
  &lt;span class="na"&gt;permission_handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^11.3.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dio&lt;/code&gt; handles the actual download with progress reporting and robust error handling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;path_provider&lt;/code&gt; gives you the platform's correct directories — the ones that exist on the device, not paths you guess.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;permission_handler&lt;/code&gt; lets you ask for storage permission cleanly on Android.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's jump into the coding part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Where Files Are Allowed to Go
&lt;/h2&gt;

&lt;p&gt;Android and iOS have completely different rules about where a downloaded file may live, and this is the heart of the whole article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On iOS&lt;/strong&gt;, there is one real answer: the Documents directory (visible in the Files app) or, for media, the app's own sandbox. Users can't browse the whole filesystem anyway, so you save to a directory your app owns and expose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Android&lt;/strong&gt;, since Android 10 (API 29), scoped storage means your app cannot just write anywhere. The honest options are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App-specific external directory&lt;/strong&gt; — &lt;code&gt;getExternalStorageDirectory()&lt;/code&gt;. No permission needed, but the folder is hidden from the user's file manager on many devices unless your app exposes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The public Downloads folder&lt;/strong&gt; — via the MediaStore or &lt;code&gt;getDownloadsDirectory()&lt;/code&gt;. This is what users expect when they tap "download": the file lands in &lt;code&gt;Download/&lt;/code&gt; and shows up in their file manager. This is the target I use for anything the user is meant to keep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule I follow: save to the app's own directory for internal artifacts, save to the public Downloads folder when the user explicitly asked to download something they will open later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Android Permissions
&lt;/h2&gt;

&lt;p&gt;For the public Downloads folder on modern Android, the permission story is more forgiving than people expect — Android 10+ allows writing to Downloads via MediaStore without the old &lt;code&gt;WRITE_EXTERNAL_STORAGE&lt;/code&gt; monster permission for most cases. But you will still hit permission requests depending on the device and Android version, so handle it cleanly:&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="c"&gt;&amp;lt;!-- android/app/src/main/AndroidManifest.xml --&amp;gt;&lt;/span&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;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.WRITE_EXTERNAL_STORAGE"&lt;/span&gt;
    &lt;span class="na"&gt;android:maxSdkVersion=&lt;/span&gt;&lt;span class="s"&gt;"28"&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;The &lt;code&gt;maxSdkVersion="28"&lt;/code&gt; matters. On Android 9 and below the old storage permission applies. On Android 10 and above scoped storage replaces it, and declaring the permission unconditionally triggers confusing runtime prompts on devices that no longer need it. This single attribute has saved me a surprising amount of support-ticket pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The Download Function
&lt;/h2&gt;

&lt;p&gt;Here is a complete, production-shaped download using &lt;code&gt;dio&lt;/code&gt;, with a progress callback and explicit save locations:&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:dio/dio.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:path_provider/path_provider.dart'&lt;/span&gt;&lt;span class="o"&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;String&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;downloadFile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&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;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;,&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;Dio&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;Directory&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dir&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;getDownloadsDirectory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&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;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dir&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="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Fallback for platforms without a Downloads directory.&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;savePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${(dir ?? await getApplicationDocumentsDirectory()).path}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;$fileName&lt;/span&gt;&lt;span class="s"&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;Response&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;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;savePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onReceiveProgress:&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;received&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;total&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;total&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;onProgress&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&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="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;statusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;savePath&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="kc"&gt;null&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;A few details worth your attention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getDownloadsDirectory()&lt;/code&gt; comes from &lt;code&gt;path_provider&lt;/code&gt; and returns the platform's real downloads location. On Android it maps to the public Downloads folder; on iOS it returns the app's Documents directory so the file appears in the Files app.&lt;/li&gt;
&lt;li&gt;The fallback to the documents directory covers desktops and any platform quirk where a Downloads directory does not exist.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;onReceiveProgress&lt;/code&gt; callback is what lets you render a progress bar. Pass it straight into a &lt;code&gt;ValueNotifier&lt;/code&gt; or a state object and the UI updates live.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dio.download&lt;/code&gt; streams the file to disk instead of holding the whole thing in memory, so a 500 MB file will not OOM your app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Calling It From the UI
&lt;/h2&gt;

&lt;p&gt;The wiring is straightforward. Here is a minimal screen that downloads a file and reports progress:&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;DownloadButton&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DownloadButton&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="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DownloadButton&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;_DownloadButtonState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_DownloadButtonState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DownloadButton&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;_progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_downloading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="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;_startDownload&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;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_downloading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;_progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;path&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;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;url:&lt;/span&gt; &lt;span class="s"&gt;'https://example.com/files/invoice.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;fileName:&lt;/span&gt; &lt;span class="s"&gt;'invoice.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;onProgress:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_downloading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;ScaffoldMessenger&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;showSnackBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;SnackBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;content:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'Downloaded to &lt;/span&gt;&lt;span class="si"&gt;$path&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'Download failed. Check your connection.'&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="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;mainAxisSize:&lt;/span&gt; &lt;span class="n"&gt;MainAxisSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_downloading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;LinearProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;value:&lt;/span&gt; &lt;span class="n"&gt;_progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="n"&gt;FilledButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;_startDownload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Download invoice'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a complete, working download flow: tap, progress, file on the device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: File Names and Collisions
&lt;/h2&gt;

&lt;p&gt;The last detail that separates a demo from a production app is file naming. Two users downloading the same invoice, or one user downloading the same report twice a day, produce collisions — and depending on the platform, the second file either silently overwrites the first or fails. The pattern I use:&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:path/path.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&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;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;uniqueSavePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Directory&lt;/span&gt; &lt;span class="n"&gt;dir&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;fileName&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;File&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${dir.path}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;$fileName&lt;/span&gt;&lt;span class="s"&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="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&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;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&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;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileName&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="kd"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;basenameWithoutExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&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;candidate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${dir.path}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;$base&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;$i&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;$ext&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you &lt;code&gt;report.pdf&lt;/code&gt;, &lt;code&gt;report(1).pdf&lt;/code&gt;, &lt;code&gt;report(2).pdf&lt;/code&gt; — the convention every file manager already uses, so users instantly understand it. Overwriting a user's previous download without asking is a data-loss bug; never let the second download silently clobber the first. If you prefer, you can also ask first with a dialog — but in my experience the &lt;code&gt;(1)&lt;/code&gt; suffix pattern removes the question entirely and users prefer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;p&gt;Here is what will bite you in production, in rough order of frequency:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scoped storage is not optional.&lt;/strong&gt; On Android 10+, writing to arbitrary paths returns errors or silently no-ops. Always use &lt;code&gt;getDownloadsDirectory()&lt;/code&gt; or the MediaStore instead of hardcoding &lt;code&gt;/storage/emulated/0/Download&lt;/code&gt;. If you hardcode, you will get "download succeeded" in the log and "file not found" in the UI — I have seen exactly that combination in a production app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;getDownloadsDirectory()&lt;/code&gt; returns null on some platforms.&lt;/strong&gt; Android can return null on certain devices and API levels. The fallback I showed is not a nice-to-have; it is the difference between a crash and a working download on those devices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HTTPS is mandatory on modern platforms.&lt;/strong&gt; Android 9+ and iOS both block cleartext HTTP by default. If your file URL is &lt;code&gt;http://&lt;/code&gt;, the download will fail with a connection error. Serve files over HTTPS, or the fix is a per-domain network-security exception, which you should avoid shipping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Large files need a resume story.&lt;/strong&gt; A 500 MB download that dies at 90% because the user locked their phone is a bad day. &lt;code&gt;dio&lt;/code&gt; supports &lt;code&gt;DownloadRange&lt;/code&gt; for resumable downloads — persist the received byte count and resume from there. It is more code, and it is worth it for anything over a few tens of megabytes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The user may want to share the file.&lt;/strong&gt; After download, use &lt;code&gt;share_plus&lt;/code&gt; to let them send it. Combine the two and you have the flow every invoice, report, and receipt app wants.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;iOS background downloads are a separate topic.&lt;/strong&gt; The &lt;code&gt;dio&lt;/code&gt; approach keeps your app foreground. If a client asks for "download even when the app is closed," that is a background transfer session on the native side and a much bigger project. Say so up front.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sanitize the file name from the server.&lt;/strong&gt; If your download URL ends in a user-controlled name, strip slashes, spaces, and path separators before building the save path. A name like &lt;code&gt;../../etc/something&lt;/code&gt; on an unsanitized path is a vulnerability you do not want to debug after the fact.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A Workflow Note From the Field
&lt;/h2&gt;

&lt;p&gt;I hit this problem most often in document-heavy apps — a logistics client wanted field agents to pull delivery manifests and store them offline, and an accounting client wanted report exports to land in Downloads for their auditors. When the requirements get that specific — exact file names, deterministic folders, resume behavior — I sketch the whole flow end to end before writing the app code, the same way I prototype an automation in a builder like &lt;a href="https://misar.dev" rel="noopener noreferrer"&gt;misar.dev&lt;/a&gt; before committing to implementation. The pattern holds: decide where files live, decide what happens on failure, then write the thirty lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does this work for media files that should appear in the gallery?&lt;/strong&gt; Partially. For photos and videos, the correct target on Android is the MediaStore (&lt;code&gt;MediaStore.Images&lt;/code&gt; / &lt;code&gt;MediaStore.Video&lt;/code&gt;) so they register in the gallery, not just the Downloads folder. That is a slightly different code path from the one above — same &lt;code&gt;dio&lt;/code&gt; download, different destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I download multiple files at once?&lt;/strong&gt; Yes, and you should not overthink it — run several &lt;code&gt;dio.download&lt;/code&gt; calls in parallel and report aggregate progress. What you should not do is spin up a thread per file on the platform side; Dart's async model handles concurrency natively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need &lt;code&gt;permission_handler&lt;/code&gt; at all?&lt;/strong&gt; For downloads to the app's own directories and to the public Downloads folder on Android 10+, usually not. The permission code is a fallback for older Android versions and specific device quirks. Keep it for the maxSdkVersion=28 path, not for the modern one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about storing the download in a database?&lt;/strong&gt; For offline-first apps, the pattern is to save the file to the app's documents directory and store its path in your database, rather than relying on the public Downloads folder. That way your app can open it without user permission, and the user-facing export is a separate optional step.&lt;/p&gt;

&lt;p&gt;That's it — a complete file-download implementation in Flutter that lands files where users can actually find them, with progress reporting, collision-safe naming, and the permission handling that keeps it working on modern Android.&lt;/p&gt;

&lt;p&gt;I have also written about file uploads to storage services and handling large media in Flutter — comment below with your download use case and I'll cover it next.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
    <item>
      <title>The Best Self-Hosted LLMs in 2026 — and How I Deployed Them</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/the-best-self-hosted-llms-in-2026-and-how-i-deployed-them-24ie</link>
      <guid>https://dev.to/mryadavgulshan/the-best-self-hosted-llms-in-2026-and-how-i-deployed-them-24ie</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical field guide to running open-weight LLMs on your own hardware — which models, which serving stacks, and the cost math nobody puts on a blog post.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A fintech client called me in January with a problem that is getting more common by the month: their compliance team had banned every external AI API. No data leaves the building, they said. The analyst who wanted to chat with their internal documents would just have to wait. I told them they did not have to wait — they could run the model on hardware they already owned.&lt;/p&gt;

&lt;p&gt;Three weeks later we had a 70B-class model serving the entire company on two GPU boxes, handling their document Q&amp;amp;A, their support triage, and a chunk of their internal code review. The cost per million tokens was a fraction of what the hosted API would have billed, the data never left the office, and the compliance team slept better. This article is that deployment, generalized: how I think about open-weight models, which ones I actually reach for, how I serve them, what the hardware math looks like, and the honest list of things that go wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Self-Host at All
&lt;/h2&gt;

&lt;p&gt;Let me state the case plainly, because there are good reasons and bad reasons to self-host, and you should know which one you have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good reasons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data sovereignty.&lt;/strong&gt; The model weights are open and your inference runs on your hardware. No prompts, no logs, no context ever crosses your perimeter. For regulated industries — finance, health, legal, defense — this is not a preference, it is a requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost at scale.&lt;/strong&gt; If you run heavy inference volumes, the per-token cost of a hosted API compounds into numbers that buy you a GPU every few months. I have watched clients burn through a server's worth of API spend inside one quarter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency and predictability.&lt;/strong&gt; No network hop, no shared-tenant queue, no rate limits. A local model responds in the same milliseconds whether it is 9 a.m. or 3 a.m., and you control exactly what runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control.&lt;/strong&gt; You own the model version, the quantization, the prompts, and the upgrade schedule. Nothing on the provider side changes under you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The bad reasons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"It's cheaper."&lt;/strong&gt; Up front it is not. GPUs are expensive, electricity is real, and your time is not free. Self-hosting only pays off at sustained volume, and I will give you the math to check your own number below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"It's more private" without knowing what private means.&lt;/strong&gt; If your concern is a specific compliance rule, verify self-hosting actually satisfies it before buying hardware. Some privacy requirements are about how the vendor handles data, and a well-contracted vendor may already meet them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We want the best model."&lt;/strong&gt; If your task needs the absolute state of the art, self-hosted open weights are months behind the biggest closed frontier models, and for some workloads that gap is the difference between useful and not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Decide which column you are in before you spend a rupee on silicon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Model Landscape in 2026
&lt;/h2&gt;

&lt;p&gt;The open-weight ecosystem moves fast, but the tiers have been stable enough to reason about. Here is how I categorize what is available today, and the hardware each tier realistically needs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Representative models&lt;/th&gt;
&lt;th&gt;What it is good at&lt;/th&gt;
&lt;th&gt;VRAM you need&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small (1–4B)&lt;/td&gt;
&lt;td&gt;Phi-4, Llama 3.2 3B, Qwen 2.5 3B&lt;/td&gt;
&lt;td&gt;Classification, extraction, routing, single-purpose tasks&lt;/td&gt;
&lt;td&gt;4–8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid (7–14B)&lt;/td&gt;
&lt;td&gt;Llama 3.1/3.3 8B, Qwen 2.5 14B, Mistral 7B&lt;/td&gt;
&lt;td&gt;General chat, summarization, light reasoning on one GPU&lt;/td&gt;
&lt;td&gt;10–24 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large (30–40B)&lt;/td&gt;
&lt;td&gt;Qwen 2.5 32B, Llama 3.1 70B (quantized)&lt;/td&gt;
&lt;td&gt;Strong reasoning, coding, agent loops on a single big GPU or two&lt;/td&gt;
&lt;td&gt;24–48 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontier open (70B+)&lt;/td&gt;
&lt;td&gt;Llama 3.3 70B, DeepSeek R1 distill variants, Qwen 3 flagship&lt;/td&gt;
&lt;td&gt;Best open quality; needs multi-GPU or heavy quantization&lt;/td&gt;
&lt;td&gt;48 GB+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest guidance: most production workloads do not need the frontier tier. A routing model that decides which sub-agent gets a task, an extraction model that reads invoices, a summarizer for internal reports — those are 3B to 8B jobs that run on a single GPU and answer in a few hundred milliseconds. Save the big models for the tasks where reasoning quality actually changes the business outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Serving Stack, Ranked by Where You Are
&lt;/h2&gt;

&lt;p&gt;The model file is only half the story. The serving layer decides your throughput, your latency, and how much of your weekend you spend fighting it. My recommendations, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ollama — the start-here option.&lt;/strong&gt; A single binary, a &lt;code&gt;pull&lt;/code&gt; command, and an OpenAI-compatible endpoint out of the box. Perfect for one machine, one developer, or a small team that just needs a local model. It is not a serious production serving layer at scale — it will get you to the demo, then you migrate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;llama.cpp — the edge and CPU option.&lt;/strong&gt; Runs quantized GGUF models on CPU, on Macs, on laptops, and even on Raspberry Pi-class hardware. When a client needs a model in the field with no GPU, llama.cpp is the tool. Expect modest tokens per second — fine for interactive use, wrong for high-throughput serving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vLLM — the production choice.&lt;/strong&gt; A continuous-batching serving engine that is OpenAI-API-compatible and designed to maximize throughput on GPUs. This is what I reach for the moment an app goes behind an endpoint that real users hit. PagedAttention keeps the GPU busy, and the API surface means your existing OpenAI client code does not change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text Generation Inference (TGI)&lt;/strong&gt; — Hugging Face's serving stack. Also excellent, also OpenAI-compatible, a reasonable alternative to vLLM. I choose between them based on which has better support for the specific model and quantization I am running that week; they are close enough that your choice of one over the other rarely determines success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TensorRT-LLM / DeepSpeed&lt;/strong&gt; — for the teams with the engineering time to squeeze the last bit of performance and the models that benefit from it. If you do not have a full-time inference engineer, you do not need these.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardware Math
&lt;/h2&gt;

&lt;p&gt;Let me give you the numbers I actually use, because every tutorial says "you need a good GPU" and then stops. The math is simple: a model needs roughly its parameter count multiplied by the bytes per weight, plus the KV cache for concurrency.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FP16: 2 bytes per parameter. A 7B model needs ~14 GB of VRAM just for weights.&lt;/li&gt;
&lt;li&gt;INT8 (AWQ or GPTQ): 1 byte per parameter. 7B → ~7 GB.&lt;/li&gt;
&lt;li&gt;4-bit quantization (GGUF Q4): ~0.5 bytes per parameter. 7B → ~4 GB; a 70B → ~40 GB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a single 24 GB RTX 4090 comfortably serves a 14B model in FP16 or a 70B model in 4-bit. Two 4090s handle a 70B with room for a real KV cache. One 48 GB workstation card or an A100 80 GB gives you frontier-tier headroom. A Mac with 64–128 GB of unified memory runs 70B-class GGUF models through llama.cpp with surprising grace — slower than a GPU, but silent and quiet in an office in a way two GPUs blasting in a rack never are.&lt;/p&gt;

&lt;p&gt;The rule of thumb I give clients: &lt;strong&gt;the KV cache is the part people forget.&lt;/strong&gt; Two users asking long questions can double your memory usage. Size your server for your concurrency, not for one benchmark prompt. I have seen a perfectly tuned single-user demo fall over at five concurrent users because the cache had nowhere to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Production Deployment (vLLM)
&lt;/h2&gt;

&lt;p&gt;Here is the deployment pattern I have shipped most often. On the GPU server, install vLLM and pull the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;vllm

vllm serve Qwen/Qwen2.5-32B-Instruct-AWQ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 32768 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quantization&lt;/span&gt; awq &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.92
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few choices worth explaining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The AWQ variant.&lt;/strong&gt; Quantized to 4-bit at the model level, so a 32B model fits comfortably on one 24 GB GPU with room for the KV cache. Quality loss on real tasks is small, and I measure it before shipping rather than assuming it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--gpu-memory-utilization 0.92&lt;/code&gt;.&lt;/strong&gt; Leave a sliver of VRAM for the CUDA runtime and the context buffers. Setting it to 1.0 is how you get OOM crashes at the worst possible moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--max-model-len 32768&lt;/code&gt;.&lt;/strong&gt; This is the ceiling of your context window, and it directly sets KV-cache memory. Do not default to the model's maximum; set what your application actually needs and your concurrency will thank you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That command gives you an OpenAI-compatible endpoint at &lt;code&gt;http://localhost:8000/v1&lt;/code&gt;. Your application code does not change:&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;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not-needed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# vLLM is unauthenticated by default
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen2.5-32B-Instruct-AWQ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You summarize internal support tickets.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this ticket: ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;api_key&lt;/code&gt; line is the sneaky detail — your existing code that hardcodes &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; keeps working against a local server. That compatibility is exactly why vLLM is my default: migration from a hosted API becomes a one-line &lt;code&gt;base_url&lt;/code&gt; change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Reality: What Goes Wrong
&lt;/h2&gt;

&lt;p&gt;Self-hosting is not "free AI." It is a new production system with its own failure modes. The list, in order of how much they have cost me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The demo dies under concurrency.&lt;/strong&gt; Single-user latency looks great; five users queue. The cause is almost always KV-cache memory or the serving engine not batching. Fix: set &lt;code&gt;--max-model-len&lt;/code&gt; to what you need, add a second GPU if the cache is the constraint, and load-test with your real prompt lengths before day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quantization quality is assumed, not measured.&lt;/strong&gt; You save 75% of VRAM with Q4 and never check whether the model still does your task. Fix: build a 50-question eval set, run it on FP16 and Q4, and compare. I have shipped models where the answer was identical on 95% of questions — and I have found tasks (exact number extraction is a classic) where quantization quietly breaks them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model changes are not versioned.&lt;/strong&gt; Someone runs &lt;code&gt;ollama pull latest&lt;/code&gt; and the "same model" suddenly behaves differently in production. Fix: pin exact model and quantization versions, store them in your config, and treat model upgrades like any other breaking deploy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The GPU is idle at night and hot at noon.&lt;/strong&gt; If your traffic is spiky, you are paying for idle silicon and burning electricity. Fix: size for your 95th percentile, put a load-balancing layer in front, and if your peaks are rare, honestly compare against a hosted option for the overflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Nobody owns the stack.&lt;/strong&gt; A self-hosted model is a server. It needs patching, backups, monitoring, and a person on call. The team that says "we self-host to save money" and has no one who understands the box usually ends up paying more in emergency engineering than they saved in API bills.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embedding models get forgotten.&lt;/strong&gt; Your LLM is self-hosted, but if your RAG pipeline still embeds with an external API, you have not actually achieved data sovereignty. Self-host your embedding model too — the same vLLM server can serve it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When NOT to Self-Host
&lt;/h2&gt;

&lt;p&gt;The honest boundary, stated so you can check yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spiky, unpredictable traffic.&lt;/strong&gt; A hosted API scales to zero when you are idle and to a thousand when you are viral. A GPU does not. If your peak-to-average ratio is wild, self-hosting is a cost problem you did not need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need the absolute state of the art.&lt;/strong&gt; The biggest closed models are ahead of open weights on the hardest tasks. If the task quality is the constraint, that gap decides it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have no one to run the box.&lt;/strong&gt; If this is a side project and the hardware is the "fun part," fine. If it is a business workload and the person who deployed it is the only one who understands it, you have a single point of failure wearing a T-shirt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small volume, one-off jobs.&lt;/strong&gt; For a few thousand tokens a month, the setup effort alone exceeds the API bill. Do the math with your own number before you buy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Practitioner's Checklist
&lt;/h2&gt;

&lt;p&gt;Before you call a self-hosted deployment done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] You wrote down the compliance or cost reason — and it is a real one, not fashion&lt;/li&gt;
&lt;li&gt;[ ] The model tier matches the task; you did not buy a 70B to extract dates from invoices&lt;/li&gt;
&lt;li&gt;[ ] VRAM is sized for weights plus KV cache at your real concurrency, not one benchmark prompt&lt;/li&gt;
&lt;li&gt;[ ] Quantization is chosen and validated against your own 50-question eval set&lt;/li&gt;
&lt;li&gt;[ ] Model and quantization versions are pinned, not "latest"&lt;/li&gt;
&lt;li&gt;[ ] The endpoint is OpenAI-compatible so app code migrates with a one-line change&lt;/li&gt;
&lt;li&gt;[ ] Embeddings are self-hosted too if data sovereignty is the actual goal&lt;/li&gt;
&lt;li&gt;[ ] Load-testing was done with real prompt lengths and real concurrency&lt;/li&gt;
&lt;li&gt;[ ] Monitoring exists: latency, throughput, VRAM, and error rate&lt;/li&gt;
&lt;li&gt;[ ] A named person owns patching, backups, and the on-call rotation&lt;/li&gt;
&lt;li&gt;[ ] You have a written fallback to a hosted API if the box dies or the demand spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The compliance-banned fintech client now runs a 32B Qwen model on one 24 GB GPU, serving the whole company, with the data never leaving the building. Their monthly bill for the box is a fixed number they can budget for; the API they replaced would have been metered and unpredictable. That is the real value of self-hosting — not just cheaper tokens, but ownership.&lt;/p&gt;

&lt;p&gt;Start where I start: pick the smallest model that does the job, serve it with Ollama to prove the workflow, move to vLLM when real users show up, and write your own cost math before you buy a single GPU. The models will keep improving; the pattern will not.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>technology</category>
    </item>
    <item>
      <title>Biometric Authentication (Face ID + Fingerprint) in Flutter</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Thu, 27 Aug 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/biometric-authentication-face-id-fingerprint-in-flutter-2ik9</link>
      <guid>https://dev.to/mryadavgulshan/biometric-authentication-face-id-fingerprint-in-flutter-2ik9</guid>
      <description>&lt;p&gt;So, in this article, I will be showing you how you can add biometric authentication — Face ID and fingerprint — to your Flutter app using the &lt;code&gt;local_auth&lt;/code&gt; plugin.&lt;/p&gt;

&lt;p&gt;This is one of the most common requests I get from mobile clients, and for good reason: a login screen with a fingerprint is expected in any finance, health, or messaging app today. I have shipped this exact flow into production apps, and it is simpler than most people expect — and it hides a handful of platform-specific traps that will silently break your build if you skip the setup steps.&lt;/p&gt;

&lt;p&gt;For this purpose, we need to add these dependencies in your &lt;code&gt;pubspec.yaml&lt;/code&gt; file:&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;local_auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^2.2.0&lt;/span&gt;
  &lt;span class="na"&gt;flutter_secure_storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^9.2.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;local_auth&lt;/code&gt; is the official plugin that talks to the platform's biometric APIs — Touch ID, Face ID, and Android's fingerprint and face unlock.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flutter_secure_storage&lt;/code&gt; stores the auth token securely after the user authenticates, so you never keep credentials in plain text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's jump into the coding part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Platform Setup (This Is Where Most Builds Break)
&lt;/h2&gt;

&lt;p&gt;Before you write a single line of Dart, you must configure both platforms. Skip this and the plugin will throw at runtime, or in some cases at compile time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android.&lt;/strong&gt; Add these permissions to &lt;code&gt;android/app/src/main/AndroidManifest.xml&lt;/code&gt;:&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.USE_BIOMETRIC"&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;For devices running Android 9 or lower you should also keep:&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.USE_FINGERPRINT"&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;&lt;strong&gt;iOS.&lt;/strong&gt; Face ID requires a usage description, otherwise iOS terminates your app when the prompt appears. Add this to &lt;code&gt;ios/Runner/Info.plist&lt;/code&gt;:&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;key&amp;gt;&lt;/span&gt;NSFaceIDUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;Unlock your account using Face ID.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single missing key is the #1 cause of "my biometric app crashes on iPhone" reports. Xcode will not warn you; it will just kill the app at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Check What the Device Actually Supports
&lt;/h2&gt;

&lt;p&gt;You should never assume a device has biometrics. Emulators usually have none, some Android devices have none, and some users disable it in settings. Always check first, then fail gracefully.&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:local_auth/local_auth.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;LocalAuthentication&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalAuthentication&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;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;canUseBiometrics&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="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isSupported&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;_auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDeviceSupported&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;hasEnrolled&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;_auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;canCheckBiometrics&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;isSupported&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;hasEnrolled&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;ul&gt;
&lt;li&gt;
&lt;code&gt;isDeviceSupported()&lt;/code&gt; returns true if the device has any biometric hardware.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;canCheckBiometrics&lt;/code&gt; returns true if the user has actually enrolled a fingerprint or face. A device can support biometrics while the user has enrolled nothing — and your button should not light up in that case.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Authenticate
&lt;/h2&gt;

&lt;p&gt;Now the actual authentication. The key detail here is &lt;code&gt;localizedReason&lt;/code&gt;: on iOS this is the sentence shown in the Face ID prompt, and on some Android versions it is required or the call fails.&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;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;authenticate&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="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;authenticated&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;_auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;localizedReason:&lt;/span&gt; &lt;span class="s"&gt;'Authenticate to access your account'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;options:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AuthenticationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;biometricOnly:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;stickyAuth:&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;span class="p"&gt;}&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="c1"&gt;// Platform errors, app switches, or user-cancelled.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authenticated&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;Two options worth understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;biometricOnly: false&lt;/code&gt; means the user can also authenticate with their device passcode as a fallback when the fingerprint reader fails. For most apps you want this — a locked-out user is worse than a slightly less "pure" biometric login.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stickyAuth: true&lt;/code&gt; keeps the authentication valid if the app goes to the background mid-prompt. If you are running on iOS and the user is nudged into another app for a step, this prevents the flow from dying.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Store the Session Token Securely
&lt;/h2&gt;

&lt;p&gt;Biometric authentication proves the user is who they say they are, but you still need something to hold the session. Never keep it in &lt;code&gt;SharedPreferences&lt;/code&gt; — store it in the secure enclave-backed storage:&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:flutter_secure_storage/flutter_secure_storage.dart'&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;FlutterSecureStorage&lt;/span&gt; &lt;span class="n"&gt;_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FlutterSecureStorage&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;onBiometricSuccess&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;token&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;await&lt;/span&gt; &lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="s"&gt;'auth_token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;value:&lt;/span&gt; &lt;span class="n"&gt;token&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;String&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;getStoredToken&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_storage&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="nl"&gt;key:&lt;/span&gt; &lt;span class="s"&gt;'auth_token'&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 pattern that works in production: on first login ask for the password, get a token from your backend, and store it in secure storage. On every later launch, offer the biometric button — if the fingerprint or face matches, read the token from secure storage and restore the session without the user ever typing a password again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The Login Screen That Ties It Together
&lt;/h2&gt;

&lt;p&gt;Here is the full pattern wired into a widget, the way I would actually ship it. A "Sign in with biometrics" button that only appears when the device can actually do it:&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;LoginScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LoginScreen&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="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoginScreen&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;_LoginScreenState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_LoginScreenState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoginScreen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_hasBiometrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_busy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_loadCapability&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;_loadCapability&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="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;available&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;canUseBiometrics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hasBiometrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;available&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;_onBiometricLogin&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;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_busy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ok&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;authenticate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&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;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_busy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Show password fallback; do not show an error toast for a cancel.&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Biometric match: restore the stored session token and enter the app.&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;token&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;getStoredToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_busy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushReplacement&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="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;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// No stored token — full login required this time.&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="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Sign in'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;mainAxisSize:&lt;/span&gt; &lt;span class="n"&gt;MainAxisSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_hasBiometrics&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_busy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="n"&gt;FilledButton&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;_onBiometricLogin&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;fingerprint&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nl"&gt;label:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Unlock with biometrics'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&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;_busy&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;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;TextButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* full password flow */&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Use password instead'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;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;Two details in this widget are worth copying, not retyping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The button is conditional.&lt;/strong&gt; &lt;code&gt;_hasBiometrics&lt;/code&gt; is loaded before the button is ever shown. A user on a device with no enrolled biometrics never sees a button that cannot work — that alone removes a whole class of "the fingerprint button does nothing" reviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cancel case is not an error.&lt;/strong&gt; A user who taps away from the Face ID prompt is back in the UI with the password path visible, not staring at a failure message. That is the difference between a flow users trust and one they abandon.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;p&gt;Here are the things that will bite you in production, in the order I have seen them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test on a real device.&lt;/strong&gt; Biometrics do not work reliably on emulators and simulators. iOS Simulator can fake Face ID via &lt;em&gt;Features → Face ID → Matching Enrolled Face&lt;/em&gt;, but Android emulators frequently report nothing enrolled. Your CI and your QA both need a physical device in the rotation, or you will ship a login flow you have never actually exercised.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Handle the cancel case explicitly.&lt;/strong&gt; When the user taps "Cancel" in the Face ID dialog, &lt;code&gt;local_auth&lt;/code&gt; throws &lt;code&gt;PlatformException&lt;/code&gt; with code &lt;code&gt;NotAuthenticated&lt;/code&gt;. Swallow it and show your password fallback. What you must not do is show a generic error toast — a user who simply changed their mind is the most common event in the entire flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;biometricOnly: true&lt;/code&gt; is rarely what you want.&lt;/strong&gt; Without a passcode fallback, a user whose fingerprint reader has been blocked (iOS locks it after several failed attempts) is completely locked out of your app until they reboot and re-enter their passcode. On a finance app, that is a support ticket. Use the fallback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Biometrics can be revoked.&lt;/strong&gt; If the user deletes a fingerprint or face after enrolling, the OS handles the prompt, but your stored session token is still valid. On a sensitive app, re-check &lt;code&gt;canCheckBiometrics&lt;/code&gt; before critical actions — not just at login — so an account whose device auth was removed still gets re-verified for high-value transactions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not use biometrics as your only security layer.&lt;/strong&gt; The fingerprint proves the device holder's identity on that device; it does not prove much about anything else. Real apps pair it with a server-issued token that has its own expiry, and revoke that token server-side on suspicious activity. The client-side biometric is the front door, not the safe.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enrollment can change while your app is backgrounded.&lt;/strong&gt; If the user re-enrolls their fingerprints in Settings and returns to your app, iOS may consider the old authentication stale. If your app supports critical actions, re-run &lt;code&gt;authenticate&lt;/code&gt; for those actions rather than trusting a login that happened an hour ago.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know your platform's error codes.&lt;/strong&gt; Android 11+ and iOS both return specific errors for hardware-not-present, hardware-unavailable, and lockout states. Log them and map them to user messages — "Biometrics unavailable, use your passcode" is honest; "Unknown error" is not.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Alternative Approaches
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;local_auth&lt;/code&gt; does not fit, two other roads exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package that wraps native SDKs&lt;/strong&gt; — some enterprise clients want the bank-grade wrappers that vendor SDKs provide. They add native code, so you are now maintaining platform channels instead of just a plugin, and you should have a concrete compliance reason before choosing this path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform channels for bespoke flows&lt;/strong&gt; — when you need liveness detection or a custom enrollment UI that the plugin does not expose, you drop to native Kotlin/Swift and call back into Dart. This is real work, and you should only take it when a client's requirements genuinely exceed what &lt;code&gt;local_auth&lt;/code&gt; offers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default answer for 95% of apps is &lt;code&gt;local_auth&lt;/code&gt;. It is maintained, null-safe, covers Face ID, Touch ID, and Android fingerprint and face unlock, and it keeps all the messy native code behind one clean Dart API.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does this work on the iOS Simulator?&lt;/strong&gt; Only for Face ID, and only if you use the &lt;em&gt;Features → Face ID → Matching Enrolled Face&lt;/em&gt; menu item. Android emulators generally report no enrolled biometrics, so assume the happy path only works on physical hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Touch ID still need the usage description?&lt;/strong&gt; No — &lt;code&gt;NSFaceIDUsageDescription&lt;/code&gt; is required for Face ID specifically. But if you only support Touch ID and later add Face ID, iOS will crash at runtime until the key is added, so add it up front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the user has no passcode set at all?&lt;/strong&gt; Then biometrics cannot enroll, and &lt;code&gt;canCheckBiometrics&lt;/code&gt; returns false. Your fallback button handles this automatically because it checks capability before rendering.&lt;/p&gt;

&lt;p&gt;That's it — a complete biometric authentication flow in Flutter. Platform setup, capability checks, the authenticate call, a secure place for the session token, and a login screen that only offers biometrics to devices that can actually use them.&lt;/p&gt;

&lt;p&gt;I have also written about secure storage, secure text fields, and app-locking patterns — comment below with your own use case and I'll cover it next.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
    <item>
      <title>Why Vector Databases Are Critical for RAG Systems</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Thu, 27 Aug 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/why-vector-databases-are-critical-for-rag-systems-a9g</link>
      <guid>https://dev.to/mryadavgulshan/why-vector-databases-are-critical-for-rag-systems-a9g</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical look at the retrieval layer of RAG — what a vector database actually does, why keyword search fails, and what it costs to get it wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few weeks ago, a health-tech client sent me a screenshot that made me stop typing. His RAG system had answered a doctor's question about a medication interaction with a dosing suggestion that was wrong — not subtly wrong, dangerously wrong. The team's first instinct was to blame the model. I asked one question: "How is your system retrieving the context?" Nobody knew. We opened the code and found it: keyword search over their knowledge base, feeding the top hits into the prompt. There was no vector database anywhere in the pipeline.&lt;/p&gt;

&lt;p&gt;That answer explained everything. The model had done its job. It read three irrelevant chunks and produced a confident, grounded-sounding, completely wrong answer. The retrieval was broken — there was no way for it to find meaning, only matching strings.&lt;/p&gt;

&lt;p&gt;That incident is why I am writing this. Vector databases are not a trendy accessory you bolt onto an LLM project. They are the difference between a RAG system that quotes the right source and one that confidently quotes the wrong one. This article covers what a vector database actually does under the hood, the taxonomy of options, a production architecture, real code, and the honest failure modes — including when you should not use one at all.## The Problem Keyword Search Can't Solve&lt;/p&gt;

&lt;p&gt;To understand why vector databases exist, you have to understand the exact failure that killed my client's system. Keyword search — BM25, SQL &lt;code&gt;LIKE&lt;/code&gt;, Elasticsearch — matches exact strings. It is brilliant at that, and terrible at meaning.&lt;/p&gt;

&lt;p&gt;Consider two sentences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"The doctor recommended a lower dose for patients with impaired kidney function."&lt;/li&gt;
&lt;li&gt;"Renal patients should receive a reduced amount."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keyword search sees almost no shared tokens between them — "lower" and "reduced" match, nothing else does — so it scores them as near-total strangers and drops the chunk that actually contains the answer. The retrieval layer hands the LLM the wrong documents, and the LLM makes the best of garbage.&lt;/p&gt;

&lt;p&gt;The deeper point: in RAG the retrieval step is the source of truth the model is bound to. When the retrieved context is correct, the model will not invent facts it cannot support — but when the context is wrong, it will not know it is wrong either. Retrieval quality sets a hard ceiling on the whole system, and no amount of prompt engineering, fine-tuning, or fancier models raises it. They only make the errors sound more confident.&lt;/p&gt;

&lt;p&gt;That is the entire argument for vector databases in one paragraph: search by meaning instead of by exact string.## How Embeddings Actually Work&lt;/p&gt;

&lt;p&gt;A vector database stores vectors, and vectors come from embedding models. Here is the mechanism in one paragraph: an embedding model converts a piece of text into a list of numbers — usually 768 to 3,072 floats — arranged so that semantically similar texts land close together in that high-dimensional space. "The dog chased the ball" and "the canine ran after the toy" produce vectors that are nearly parallel, even though they share almost no words. Distance in that space is a proxy for difference in meaning; the famous toy example — &lt;code&gt;king − man + woman ≈ queen&lt;/code&gt; — is the same idea in arithmetic form.&lt;/p&gt;

&lt;p&gt;The critical practical detail is that the embedding model you use is a permanent architectural decision. You index documents with it, then you query with it. Change the model later and every vector in the store is in the wrong language for the new model — you must re-embed the entire corpus. Decide early, measure the retrieval quality on your own data, and treat the choice as locked-in until you have a hard reason to pay for a re-embed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Vector Database Actually Does
&lt;/h2&gt;

&lt;p&gt;Here is the part that confuses people: the vector database is not doing anything clever with meaning. The embedding model produced the meaning; the database just makes searching it fast and correct. Its job is to solve one specific problem — finding the k nearest neighbors of a query vector among millions of vectors, in milliseconds, at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Approximate Nearest Neighbor Trade-Off
&lt;/h3&gt;

&lt;p&gt;Finding the exact nearest neighbor in a million-vector space is too slow for real-time retrieval. So vector databases use Approximate Nearest Neighbor (ANN) algorithms that trade a small amount of accuracy for orders of magnitude in speed. The two you will actually meet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HNSW (Hierarchical Navigable Small World)&lt;/strong&gt; — a graph-based index. Fast at query time, memory-hungry (the whole graph sits in RAM), excellent for most RAG workloads. This is the default in Qdrant and the go-to choice for most production systems I have built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IVF (Inverted File)&lt;/strong&gt; — clusters vectors into partitions and only searches the nearest ones. Uses less memory, slightly slower, better when the index is too large for RAM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical guidance I give clients: start with HNSW — easier to tune, faster to query, and your corpus needs to be very large before memory cost becomes the thing that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distance Functions Matter
&lt;/h3&gt;

&lt;p&gt;The measure of "near" is configurable, and getting it wrong silently degrades retrieval. The three you will see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cosine similarity&lt;/strong&gt; — measures the angle between vectors, ignores magnitude. The standard default for RAG; most text embeddings behave best with cosine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dot product&lt;/strong&gt; — faster, sensitive to vector magnitude. Fine if your embedding model is normalized; otherwise it rewards long vectors for the wrong reasons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Euclidean distance&lt;/strong&gt; — measures raw distance. Works, but more sensitive to scale and less commonly the right default for text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule of thumb: if your embedding model normalizes its output vectors to unit length, dot product and cosine give identical rankings, and dot product is cheaper. If unsure, use cosine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata Filtering Is the Hidden Requirement
&lt;/h3&gt;

&lt;p&gt;Here is the requirement most tutorials skip and every production system needs: you almost never want to search the whole corpus — you want "all support tickets from the last 90 days" or "policies that apply to the EU region." A vector database that cannot filter by metadata during the ANN search forces you into a classic failure — pulling every matching vector into memory, then doing an exact search over them, which blows your latency budget on a large corpus.&lt;/p&gt;

&lt;p&gt;The right approach is index-level filtering: store region, date, document type, tenant ID, and permissions as payload fields, and let the database apply them inside the search. This is also where multi-tenancy lives — one shared store serving ten companies with no tenant filter is an information-leak bug waiting to happen. Filter by tenant, always.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vector Database Taxonomy
&lt;/h2&gt;

&lt;p&gt;Every week someone asks me which vector database to use. Here is the landscape as I actually reason about it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qdrant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dedicated vector DB, Rust, HNSW-first, metadata filtering first-class&lt;/td&gt;
&lt;td&gt;Production RAG, multi-tenant, needs fast filtered search&lt;/td&gt;
&lt;td&gt;One more service to operate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pgvector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres extension&lt;/td&gt;
&lt;td&gt;Teams already on Postgres, small-to-medium corpora, simplest ops&lt;/td&gt;
&lt;td&gt;Filtered search gets slow past a few million vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Library, not a server (from Meta)&lt;/td&gt;
&lt;td&gt;Offline indexing, research, embedding pipelines at scale&lt;/td&gt;
&lt;td&gt;You build the persistence, filtering, and serving yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Managed (Pinecone, Weaviate Cloud, etc.)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hosted vector DBs&lt;/td&gt;
&lt;td&gt;Teams that want zero ops&lt;/td&gt;
&lt;td&gt;Cost grows with volume and latency guarantees vary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest advice: if you are already running Postgres and your corpus is under a few million chunks, &lt;code&gt;pgvector&lt;/code&gt; is the right first move — one fewer moving part, and your transactions and vectors live in one place. The moment filtered search on a large index starts hurting, or you need predictable multi-tenant latency, move to a dedicated engine like Qdrant. FAISS is for building pipelines, not shipping apps; managed services are for teams whose scarce resource is time, not money.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Production-Shaped Example
&lt;/h2&gt;

&lt;p&gt;Let me make this concrete. Here is the smallest real architecture I would ship: embed with a good model, store in Qdrant, search with metadata filtering, feed the top chunks to an LLM. I will use the Qdrant client with an OpenAI-compatible embedding endpoint.&lt;/p&gt;

&lt;p&gt;First, create the collection with the right distance function and a payload schema that carries metadata:&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;qdrant_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;QdrantClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;qdrant_client.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PointStruct&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QdrantClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:6333&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;COLLECTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;knowledge_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recreate_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vectors_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COSINE&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;Then index a batch of documents, storing the text alongside its vector so you can return the actual source, not just an ID:&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;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/v1/embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-embedding-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EU&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kidney-impaired patients should receive a reduced dose.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EU&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Standard adult dosing for this compound is 40 mg once daily.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;PointStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;embed&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query with the tenant filter applied at the index level, not as an afterthought:&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="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What dose should be used for a patient with reduced kidney function?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}]},&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;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;query_filter&lt;/code&gt; line keeps tenant A's answers from leaking into tenant B's. It belongs inside the search — the reason you want a database that supports index-level filtering rather than a raw library.&lt;/p&gt;

&lt;p&gt;The full RAG call then becomes: embed the user question, retrieve the top-k chunks with the tenant filter, join them into a context block, and send that plus the question to the LLM with an instruction to answer only from the provided context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Reality: Where This Quietly Goes Wrong
&lt;/h2&gt;

&lt;p&gt;I have hit every one of these in real deployments, in the order they hurt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chunk size and overlap are treated as defaults.&lt;/strong&gt; Chunking determines what retrieval can even find. Chunks too large dilute the meaning per vector; too small lose the surrounding context. There is no universal answer — it depends on your documents — so measure it, don't inherit a magic number from a blog post. Start at 512–768 tokens with 10–15% overlap and benchmark.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;top-k set once, never revisited.&lt;/strong&gt; A &lt;code&gt;limit=3&lt;/code&gt; that worked on your demo corpus will starve a production corpus. Monitor how often the correct answer appears in the retrieved set and tune k until retrieval quality stops improving.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No hybrid search, so exact terms get lost.&lt;/strong&gt; Embeddings are bad at exact strings: part numbers, error codes, model names. A query for "ERR-1042" will return semantically similar-looking noise instead of the document that literally contains "ERR-1042". The fix is hybrid retrieval — run BM25 keyword search and vector search in parallel and merge results with a reciprocal rank fusion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-ranking is skipped for cost reasons.&lt;/strong&gt; Retrieving 50 candidates and re-ranking the top 20 with a cross-encoder beats just taking the top 5 from vector search. It costs latency and money, and on real user queries it is usually worth it — the single biggest retrieval quality upgrade I can make for a client.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embedding drift after a model change.&lt;/strong&gt; Upgrade the embedding model and forget the re-embed, and every query is measured against vectors in a different space. Retrieval silently degrades. Version your embeddings and re-embed before you ship.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The empty-result failure is unhandled.&lt;/strong&gt; When retrieval returns nothing relevant, many systems still feed an empty or irrelevant context to the model, which then answers from its training data as if grounded. Enforce a minimum relevance threshold and let the system say "I don't have the information" instead of hallucinating.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When NOT to Use a Vector Database
&lt;/h2&gt;

&lt;p&gt;The honest part. A vector database is not always the answer, and I have told clients so to their face:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your corpus is under a few thousand chunks and does not grow.&lt;/strong&gt; Run a brute-force cosine scan in a few lines of NumPy over an in-memory list. You do not need a server for that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your queries are dominated by exact terms — codes, IDs, SKUs, model numbers.&lt;/strong&gt; A well-indexed relational table or Elasticsearch beats embeddings there. Add vector search only if you also have meaning-based queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot maintain one more service.&lt;/strong&gt; Every service you add to a small team is a pager rotation, a backup job, and a failure surface. If &lt;code&gt;pgvector&lt;/code&gt; covers your volume, do not spin up a second database just to feel modern.
## The Practitioner's Checklist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you trust a RAG system, walk this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Retrieval is meaning-based — you have embeddings and a vector index, not keyword search alone&lt;/li&gt;
&lt;li&gt;[ ] The embedding model choice is deliberate and versioned; a change triggers a full re-embed&lt;/li&gt;
&lt;li&gt;[ ] Distance function matches your embedding model (cosine for most text models)&lt;/li&gt;
&lt;li&gt;[ ] Metadata filtering — tenant, region, date, doc type — happens inside the search, not after it&lt;/li&gt;
&lt;li&gt;[ ] Multi-tenant isolation is enforced at the index level, never by trusting the app layer&lt;/li&gt;
&lt;li&gt;[ ] Chunk size and overlap were tuned against your own eval set, not inherited&lt;/li&gt;
&lt;li&gt;[ ] top-k is monitored and tuned, not frozen at the demo value&lt;/li&gt;
&lt;li&gt;[ ] Exact-term queries are covered — hybrid search or a keyword fallback exists&lt;/li&gt;
&lt;li&gt;[ ] Retrieval is re-ranked if quality or budget allows; you measured the trade-off&lt;/li&gt;
&lt;li&gt;[ ] Empty retrieval is handled explicitly — the system says "I don't know" instead of hallucinating&lt;/li&gt;
&lt;li&gt;[ ] Retrieval quality has an evaluation set and a measured metric&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Grounded Systems Are Built on Retrieval
&lt;/h2&gt;

&lt;p&gt;Back to the health-tech client. We replaced the keyword layer with a vector store, added metadata filters so each hospital's data stayed isolated, tuned chunking against their real documents, and re-ranked the top candidates. The wrong-dosage answers stopped because retrieval started returning the right source. We changed the model nowhere and almost nothing about the prompt — the fix was entirely in the retrieval layer.&lt;/p&gt;

&lt;p&gt;That is the whole lesson, and I will repeat it every time someone blames the LLM: in RAG, the model is only as good as the context you hand it, and the context is only as good as retrieval. Build the retrieval layer well — it is the difference between sounding grounded and being grounded.&lt;/p&gt;

&lt;p&gt;Start small: get your documents into a vector index, add the tenant filter, and benchmark before you buy a second server. The day a wrong chunk reaches a clinician, the vector database was never a convenience — it was the floor.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Flutter Push Notifications with FCM — Complete 2026 Walkthrough</title>
      <dc:creator>Gulshan Yadav</dc:creator>
      <pubDate>Wed, 26 Aug 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/mryadavgulshan/flutter-push-notifications-with-fcm-complete-2026-walkthrough-d5d</link>
      <guid>https://dev.to/mryadavgulshan/flutter-push-notifications-with-fcm-complete-2026-walkthrough-d5d</guid>
      <description>&lt;p&gt;Every project I touch eventually needs push notifications, and every project asks the same question: why does the notification work in the foreground but vanish in the background? So, in this article, I will be showing you how you can set up Flutter push notifications with Firebase Cloud Messaging (FCM) the complete way — foreground, background, and terminated states — including the 2026 gotchas: the Android 13 runtime permission, the FCM HTTP v1 API, and the background handler that half the tutorials skip.&lt;/p&gt;

&lt;p&gt;For this purpose, we need to add these dependencies in your &lt;code&gt;pubspec.yaml&lt;/code&gt; file:&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;firebase_core&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^3.8.0&lt;/span&gt;
  &lt;span class="na"&gt;firebase_messaging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^15.1.0&lt;/span&gt;
  &lt;span class="na"&gt;flutter_local_notifications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^18.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;firebase_core&lt;/code&gt; initializes Firebase in your app — mandatory first step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firebase_messaging&lt;/code&gt; handles receiving FCM messages and the device token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flutter_local_notifications&lt;/code&gt; shows notifications for messages received while the app is in the &lt;em&gt;foreground&lt;/em&gt; — a step everyone forgets, because FCM does not display foreground messages by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;firebase_messaging&lt;/code&gt; package's own documentation points you to &lt;code&gt;flutter_local_notifications&lt;/code&gt; for exactly this reason, and skipping it is the #1 cause of "notifications work when the app is closed but not when I open it."&lt;/p&gt;

&lt;p&gt;Let's jump into the coding part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Firebase Project Setup
&lt;/h2&gt;

&lt;p&gt;Before any Dart, the project-side setup, because it is where most people stall:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Firebase project in the Firebase console and add your Android (package name) and iOS (bundle ID) apps.&lt;/li&gt;
&lt;li&gt;Download &lt;code&gt;google-services.json&lt;/code&gt; and drop it into &lt;code&gt;android/app/&lt;/code&gt;, and &lt;code&gt;GoogleService-Info.plist&lt;/code&gt; into &lt;code&gt;ios/Runner/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the Google services Gradle plugin to &lt;code&gt;android/build.gradle&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// android/build.gradle&lt;/span&gt;
&lt;span class="n"&gt;buildscript&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;classpath&lt;/span&gt; &lt;span class="s1"&gt;'com.google.gms:google-services:4.4.2'&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and apply it at the bottom of &lt;code&gt;android/app/build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s1"&gt;'com.google.gms.google-services'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;For iOS, update your AppDelegate to let Flutter know the app is done launching (needed for background notifications):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ios/Runner/AppDelegate.swift&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;FirebaseCore&lt;/span&gt;

&lt;span class="kd"&gt;@UIApplicationMain&lt;/span&gt;
&lt;span class="kd"&gt;@objc&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;AppDelegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;FlutterAppDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;didFinishLaunchingWithOptions&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]?)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;FirebaseApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;#available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iOS&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;UNUserNotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;UNUserNotificationCenterDelegate&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;GeneratedPluginRegistrant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&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="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;didFinishLaunchingWithOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;launchOptions&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;On iOS you must also upload your APNs key to Firebase (Project settings → Cloud Messaging) or FCM cannot deliver to iPhones. This is the most common silent failure: everything works on Android, nothing arrives on iOS, and the reason is a missing APNs key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Initialize Firebase and Request Permission
&lt;/h2&gt;

&lt;p&gt;Now the Dart side. Initialize Firebase before anything else, request notification permission, and grab the device token:&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:firebase_core/firebase_core.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:firebase_messaging/firebase_messaging.dart'&lt;/span&gt;&lt;span class="o"&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;setupPush&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;await&lt;/span&gt; &lt;span class="n"&gt;Firebase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeApp&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;messaging&lt;/span&gt; &lt;span class="o"&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;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Android 13+ requires a runtime permission (API 33+)&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;settings&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;messaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;alert:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;badge:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;sound:&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="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Granted: &lt;/span&gt;&lt;span class="si"&gt;${settings.authorizationStatus}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Register for remote messages (needed on iOS too)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;messaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setForegroundNotificationPresentationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;alert:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;badge:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;sound:&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="c1"&gt;// Get the device token to send to your backend&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;token&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;messaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Send `token` to your server and store it.&lt;/span&gt;
  &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'FCM Token: &lt;/span&gt;&lt;span class="si"&gt;$token&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;p&gt;On Android 13 and newer, &lt;code&gt;requestPermission()&lt;/code&gt; triggers the runtime dialog the OS requires — without it, notifications are silently blocked. On older Android and on iOS, this maps to the appropriate system permission. Run this in &lt;code&gt;main()&lt;/code&gt; after &lt;code&gt;WidgetsFlutterBinding.ensureInitialized()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The Foreground Message Handler
&lt;/h2&gt;

&lt;p&gt;FCM does &lt;em&gt;not&lt;/em&gt; show a notification when the app is in the foreground — it only delivers the data to your handler. So we forward it to &lt;code&gt;flutter_local_notifications&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_local_notifications/flutter_local_notifications.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;_local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FlutterLocalNotificationsPlugin&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;initLocalNotifications&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;const&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;AndroidInitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'@mipmap/ic_launcher'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;DarwinInitializationSettings&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;_local&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;init&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;showForegroundNotification&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="kd"&gt;async&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;_local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notification&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s"&gt;'Update'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notification&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;'main_channel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'General'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;channelDescription:&lt;/span&gt; &lt;span class="s"&gt;'General notifications'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;importance:&lt;/span&gt; &lt;span class="n"&gt;Importance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;priority:&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;DarwinNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your setup, listen for foreground messages and route them:&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;showForegroundNotification&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: The Background and Terminated-State Handler
&lt;/h2&gt;

&lt;p&gt;This is the part that gets skipped, and it is why notifications "disappear" when the app is closed. A message that arrives when the app is backgrounded or terminated is delivered to a &lt;strong&gt;top-level handler&lt;/strong&gt; — a function outside your widget tree, exactly like the workmanager callback in my background-tasks guide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pragma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'vm:entry-point'&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;firebaseMessagingBackgroundHandler&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="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Do NOT touch UI here. Log, or queue work.&lt;/span&gt;
  &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Background message: &lt;/span&gt;&lt;span class="si"&gt;${message.notification?.title}&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;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;Firebase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeApp&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;onBackgroundMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firebaseMessagingBackgroundHandler&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;p&gt;The &lt;code&gt;@pragma('vm:entry-point')&lt;/code&gt; annotation is essential — without it, release builds can strip the handler and background messages silently do nothing. When the app is terminated, the OS launches a &lt;em&gt;minimal isolate&lt;/em&gt; to run this handler; it has a few seconds and no UI access. If you need to handle taps that open the app, listen to &lt;code&gt;FirebaseMessaging.onMessageOpenedApp&lt;/code&gt; and handle &lt;code&gt;messaging.getInitialMessage()&lt;/code&gt; for terminated-state launches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Sending a Notification — the 2026 Way
&lt;/h2&gt;

&lt;p&gt;In 2026, send using FCM's HTTP v1 API (the legacy &lt;code&gt;send&lt;/code&gt; endpoint is deprecated). Example using the legacy-free approach with a server service account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Server side — use an OAuth2 token from your service account&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://fcm.googleapis.com/v1/projects/&amp;lt;YOUR_PROJECT_ID&amp;gt;/messages:send"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ACCESS_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "message": {
      "token": "&amp;lt;DEVICE_FCM_TOKEN&amp;gt;",
      "notification": {
        "title": "Order shipped",
        "body": "Your order #4821 is on the way"
      },
      "android": { "notification": { "channel_id": "main_channel" } }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here. First, the &lt;code&gt;android.notification.channel_id&lt;/code&gt; must match the channel name from your &lt;code&gt;AndroidNotificationDetails&lt;/code&gt; — mismatch means the notification arrives with the app's default channel and bad behavior. Second, for Android 13+ you can include &lt;code&gt;android.notification.priority&lt;/code&gt; and rely on the channel importance you set in the app, not the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important Notes — The Failure Modes I Have Paid For
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Foreground messages are never shown by FCM.&lt;/strong&gt; If you did not wire &lt;code&gt;onMessage&lt;/code&gt; into &lt;code&gt;flutter_local_notifications&lt;/code&gt;, foreground notifications appear to be "broken." They are not broken — they are being delivered to a handler that does nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android 13 blocks notifications without the runtime permission.&lt;/strong&gt; Test on a physical Android 13+ device, not an emulator. If &lt;code&gt;requestPermission()&lt;/code&gt; returns denied, no notification will ever show, and no amount of payload tuning fixes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The background handler must be top-level with &lt;code&gt;@pragma('vm:entry-point')&lt;/code&gt;.&lt;/strong&gt; A closure that references widget state will crash or no-op in the background isolate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token refresh.&lt;/strong&gt; FCM tokens rotate. Listen to &lt;code&gt;messaging.onTokenRefresh&lt;/code&gt; and update your backend, or users quietly stop receiving notifications after a reinstall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS needs the APNs key configured in Firebase&lt;/strong&gt;, or Android works and iOS is dead silent. Also register the background modes in your Xcode project if you need &lt;code&gt;data-only&lt;/code&gt; messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not trust the emulator for delivery.&lt;/strong&gt; Emulators often have unreliable FCM delivery. Physical devices behave differently; test there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep notification payloads small.&lt;/strong&gt; A 4KB notification payload is the FCM limit. If you need to send rich data, send an ID and fetch the rest over your API when the notification is tapped.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 6: Handle Token Refresh and Notification Taps
&lt;/h2&gt;

&lt;p&gt;Two pieces of the wiring complete the picture, and both are usually missing from tutorials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token refresh.&lt;/strong&gt; FCM tokens rotate — after a reinstall, an app update, or on some OS quirks. Listen for it and push the new token to your backend, or your users silently stop getting notifications:&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;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onTokenRefresh&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;newToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Send newToken to your backend, replacing the old one.&lt;/span&gt;
  &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Token refreshed: &lt;/span&gt;&lt;span class="si"&gt;$newToken&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;p&gt;&lt;strong&gt;Tap handling with navigation.&lt;/strong&gt; When the user taps a notification you usually want to deep-link somewhere. The message payload's &lt;code&gt;data&lt;/code&gt; map is where you put a route or ID, and you handle taps in the foreground and the backgrounded/terminated states separately:&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setupTapHandlers&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 was in foreground when tapped&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;message&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;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&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="s"&gt;'route'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;navigatorKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentState&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;pushNamed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;??&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;span class="c1"&gt;// App was terminated when tapped — check on launch&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;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInitialMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&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="s"&gt;'route'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="n"&gt;navigatorKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentState&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;pushNamed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;??&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;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;Keep a &lt;code&gt;GlobalKey&amp;lt;NavigatorState&amp;gt;&lt;/code&gt; at the app root so the handler can navigate even when it runs outside a widget's &lt;code&gt;context&lt;/code&gt;. Data messages (a &lt;code&gt;data&lt;/code&gt; field without a &lt;code&gt;notification&lt;/code&gt; field) are the reliable way to carry this routing payload, because FCM delivers them to your handler on every state — foreground, background, and terminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Subscribing to Topics
&lt;/h2&gt;

&lt;p&gt;Instead of sending to individual device tokens, FCM supports topics — one message fans out to every subscribed device. This is how I shipped a weekly digest for a client without managing a token list:&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;FirebaseMessaging&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;subscribeToTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'digest-weekly'&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;FirebaseMessaging&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;unsubscribeFromTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'digest-weekly'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send to a topic with the same HTTP v1 endpoint, replacing &lt;code&gt;"token"&lt;/code&gt; with &lt;code&gt;"topic": "digest-weekly"&lt;/code&gt;. Topics are perfect for broadcast-style notifications (news, offers, digests) and dead simple at scale. The catch: everyone subscribed to a topic gets the same message, so personalization still needs a token-based send.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 Checklist — Go Through This Before You Ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;google-services.json&lt;/code&gt; in &lt;code&gt;android/app/&lt;/code&gt; and Gradle plugin applied&lt;/li&gt;
&lt;li&gt;[ ] iOS: &lt;code&gt;GoogleService-Info.plist&lt;/code&gt; in &lt;code&gt;ios/Runner/&lt;/code&gt;, APNs key uploaded in Firebase&lt;/li&gt;
&lt;li&gt;[ ] iOS: &lt;code&gt;FirebaseApp.configure()&lt;/code&gt; in AppDelegate, notification delegate set&lt;/li&gt;
&lt;li&gt;[ ] Android 13+ runtime permission requested and granted on a physical device&lt;/li&gt;
&lt;li&gt;[ ] Foreground messages routed through &lt;code&gt;flutter_local_notifications&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Top-level background handler with &lt;code&gt;@pragma('vm:entry-point')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;onMessageOpenedApp&lt;/code&gt; + &lt;code&gt;getInitialMessage&lt;/code&gt; handle taps in every state&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;onTokenRefresh&lt;/code&gt; updates your backend&lt;/li&gt;
&lt;li&gt;[ ] Payload &lt;code&gt;data&lt;/code&gt; carries a route key, tested on a locked, backgrounded device&lt;/li&gt;
&lt;li&gt;[ ] Sends use the FCM HTTP v1 API, not the deprecated legacy endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data-Only Messages vs Notification Messages — Pick Deliberately
&lt;/h2&gt;

&lt;p&gt;FCM has two message shapes, and choosing wrong is the root of half the "notification disappeared" bugs I debug.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;notification message&lt;/strong&gt; (a &lt;code&gt;notification&lt;/code&gt; field) is handled by the OS when the app is backgrounded or terminated — FCM itself builds the notification, and your Dart handler is &lt;em&gt;not&lt;/em&gt; guaranteed to run for display purposes. A &lt;strong&gt;data-only message&lt;/strong&gt; (only a &lt;code&gt;data&lt;/code&gt; field, no &lt;code&gt;notification&lt;/code&gt;) is delivered to your Dart handler in &lt;em&gt;every&lt;/em&gt; state — foreground, background, and terminated — and you decide what to show, or whether to show anything.&lt;/p&gt;

&lt;p&gt;My rule: use &lt;strong&gt;notification messages&lt;/strong&gt; for simple alerts you want the OS to display with zero code, and &lt;strong&gt;data-only messages&lt;/strong&gt; whenever you need custom behavior — routing, localization, deciding on-device whether the notification is relevant. The tap-handling code above relies on &lt;code&gt;data&lt;/code&gt;, so if you only ever send notification messages, you will have nothing to read in &lt;code&gt;onMessageOpenedApp&lt;/code&gt;. When in doubt, send both: a small &lt;code&gt;notification&lt;/code&gt; for OS display plus a &lt;code&gt;data&lt;/code&gt; payload with your routing keys.&lt;/p&gt;

&lt;p&gt;For quick manual tests, the Firebase console's Cloud Messaging section lets you compose and send a message without writing any server code — target your physical device by token and confirm each state (foreground, background, terminated) before you involve the backend. That ten-minute manual pass has caught more misconfiguration for me than any amount of code reading, because it isolates the problem: if the console send fails to display, the bug is in your app wiring; if it displays but your API send does not, the bug is server-side.&lt;/p&gt;

&lt;p&gt;That's it — a complete FCM push notification integration for 2026: Firebase project setup, Android 13 permission, device token, foreground routing through local notifications, the top-level background handler, and a working HTTP v1 send. The four-file checklist is: &lt;code&gt;google-services.json&lt;/code&gt;, the Gradle plugin, the iOS AppDelegate, and the &lt;code&gt;main.dart&lt;/code&gt; wiring — and the background handler is the one that makes the difference between "works on my desk" and "works on a locked phone in a pocket."&lt;/p&gt;

&lt;p&gt;I have also covered local scheduled notifications and background tasks with this exact stack — comment below with the notification feature you are stuck on and I'll cover it next.&lt;/p&gt;




&lt;p&gt;*Gulshan Yad&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
  </channel>
</rss>
