<?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: Paragon Framework</title>
    <description>The latest articles on DEV Community by Paragon Framework (@paragonkit).</description>
    <link>https://dev.to/paragonkit</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%2F4062535%2Ffc7b3b70-82ff-4dc5-aa78-085f61c65352.png</url>
      <title>DEV Community: Paragon Framework</title>
      <link>https://dev.to/paragonkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paragonkit"/>
    <language>en</language>
    <item>
      <title>15 green checks and a stale demo</title>
      <dc:creator>Paragon Framework</dc:creator>
      <pubDate>Wed, 05 Aug 2026 14:04:48 +0000</pubDate>
      <link>https://dev.to/paragonkit/15-green-checks-and-a-stale-demo-5gj</link>
      <guid>https://dev.to/paragonkit/15-green-checks-and-a-stale-demo-5gj</guid>
      <description>&lt;p&gt;I have a release script with fifteen automated checks. It passed all fifteen. Then I opened my own live demo and the About screen showed the previous version.&lt;/p&gt;

&lt;p&gt;Here is what it checks, on the release in question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS  version 1.8.0 consistent (pubspec = CHANGELOG = app About screen)
PASS  public changelog announces v1.8 and marks it Latest
PASS  working tree clean (what you see is what packages)
PASS  no merge-conflict markers in tracked files
PASS  every local link on the sales site resolves
PASS  archive contains no internal-only files
PASS  archive contents are built from the current version (1.8.0)
PASS  the site is up (/ returns 200 and is not a Pages error page)
PASS  all 8 landing-page images load from the live site
PASS  analytics beacon present on every public page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not one of those was wrong. The version really was consistent. The changelog really was updated. The site really was up.&lt;/p&gt;

&lt;p&gt;The demo is a separately compiled build, and my deploy script publishes the marketing site. Nothing built the demo, and nothing looked at what the demo was running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every check I own is scar tissue
&lt;/h2&gt;

&lt;p&gt;The "is the site up" check exists because I once took the site offline. The "do the images load" check exists because a deploy deleted every image while the page still returned 200 — a status check alone said "fine" the whole time the logo was a broken icon.&lt;/p&gt;

&lt;p&gt;They are all reactions to something that already went wrong. Which makes a fully green run a precise statement about my past mistakes, and silent about everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;green  ==  "none of my previous mistakes recurred"
green  !=  "nothing is wrong"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those feel identical at 1am when you want to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is embarrassingly cheap
&lt;/h2&gt;

&lt;p&gt;Flutter writes a &lt;code&gt;version.json&lt;/code&gt; next to the web build, populated from pubspec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"app_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"my_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1.8.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"build_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"7"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's live, it's public, and it's a couple of hundred bytes. So you can just ask the deployed app what it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/app/version.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check compares that to the version you think you're shipping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Net.WebClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DownloadString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s1"&gt;'https://example.com/app/version.json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'"version"\s*:\s*"([0-9.]+)"'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$Matches&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pubVer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"live demo runs &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$Matches&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="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; but you are shipping &lt;/span&gt;&lt;span class="nv"&gt;$pubVer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message matters as much as the check. Mine spells out the recovery, because forgetting the steps &lt;em&gt;is&lt;/em&gt; the bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;FAIL  live demo at /app/ still runs 1.7.0 but you are shipping 1.8.0.
      Rebuild and redeploy it: flutter build web &lt;span class="nt"&gt;--base-href&lt;/span&gt; /app/,
      inject_beacon.ps1 build/web, &lt;span class="k"&gt;then &lt;/span&gt;copy into the Pages clone.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten minutes of work for a hole that had been open for who knows how many releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I fixed it in the wrong place
&lt;/h2&gt;

&lt;p&gt;This is the part worth your time, because the first failure is ordinary and this one isn't.&lt;/p&gt;

&lt;p&gt;I rebuilt the demo, copied it into the GitHub Pages repo, pushed, watched the live version flip to the new number, and moved on.&lt;/p&gt;

&lt;p&gt;Then I ran my deploy script for an unrelated change, and it put the old build back.&lt;/p&gt;

&lt;p&gt;The layout is the whole story:&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="s"&gt;site/            &amp;lt;- source of truth, synced wholesale by the deploy script&lt;/span&gt;
  &lt;span class="s"&gt;index.html&lt;/span&gt;
  &lt;span class="s"&gt;app/           &amp;lt;- the published copy of the demo  ** still 1.7.0 **&lt;/span&gt;
  &lt;span class="s"&gt;admin/&lt;/span&gt;

&lt;span class="s"&gt;paragon-demo/    &amp;lt;- the GitHub Pages repo&lt;/span&gt;
  &lt;span class="s"&gt;app/           &amp;lt;- what I fixed by hand          ** now 1.8.0 **&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had fixed the destination while the source still held the previous build. The next deploy faithfully republished the stale one, exactly as designed.&lt;/p&gt;

&lt;p&gt;For a few minutes I had a "verified fixed" demo on a countdown to breaking again, and the thing that would break it was my own tooling working correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things I took from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Verify the artifact, not the action.&lt;/strong&gt; "I deployed the fix" and "the fix is live" are different claims. I had confirmed the first and assumed the second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poll, don't peek.&lt;/strong&gt; My first "it's live now" check passed against a CDN copy that hadn't rebuilt yet. Checking once and believing it is how you confirm a lie:&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="k"&gt;until &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/version.json"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"1.8.0"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix the source, not the copy.&lt;/strong&gt; If a pipeline generates a destination from a source, editing the destination is an illusion with an expiry date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check what the user touches.&lt;/strong&gt; My pipeline was thorough about inputs: source files, version strings, archive contents, links. It had nothing to say about the compiled thing a stranger clicks, which is the only artifact that actually matters.&lt;/p&gt;

&lt;p&gt;I found this the ordinary way, by looking at my own product like a user would, hours after telling myself it was done. Every check on that list was earned the same way. This one just cost less than the others, because I got there before a customer did.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>testing</category>
      <category>flutter</category>
      <category>programming</category>
    </item>
    <item>
      <title>How I Made Features in a Large Flutter App Actually Removable</title>
      <dc:creator>Paragon Framework</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:57:51 +0000</pubDate>
      <link>https://dev.to/paragonkit/how-i-made-features-in-a-large-flutter-app-actually-removable-3jk4</link>
      <guid>https://dev.to/paragonkit/how-i-made-features-in-a-large-flutter-app-actually-removable-3jk4</guid>
      <description>&lt;p&gt;"Just delete the features you don't need" is the easiest thing in the world to write in a README, and the hardest to make true.&lt;/p&gt;

&lt;p&gt;I hit this building a Flutter app with six verticals in one codebase — marketplace, ride-hailing, car rentals, social feed, chat, wallet. Deleting one should have been simple. It wasn't, because every feature had tendrils:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a route in the central route table&lt;/li&gt;
&lt;li&gt;a tab hardcoded in the app shell&lt;/li&gt;
&lt;li&gt;a button on the home screen&lt;/li&gt;
&lt;li&gt;a service registered in &lt;code&gt;main()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remove the feature folder and you get a wall of compile errors from files that have nothing to do with it. Here's what actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make three things data instead of code
&lt;/h2&gt;

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

&lt;p&gt;Each feature exposes its own routes from its own folder:&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;WalletModule&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;AppModule&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;WalletModule&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;'wallet'&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;GetPage&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;pages&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="n"&gt;GetPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;AppRoutes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;page:&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;WalletScreen&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
      &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app's route table becomes a composition:&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;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetPage&lt;/span&gt;&lt;span class="p"&gt;&amp;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;_centralRoutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ModuleRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pages&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;Adding or removing a feature stops being an edit to a shared file. It's one line in a registry.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Bottom-navigation tabs
&lt;/h3&gt;

&lt;p&gt;This one surprised me. My app shell imported the feed widget directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before — the always-present shell depends on an optional feature&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'../feed/feed_tab.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell ships in &lt;em&gt;every&lt;/em&gt; build. That import meant the social feature could never be removed.&lt;/p&gt;

&lt;p&gt;So a tab became a small data class that a feature contributes:&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;ShellTab&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// core tabs use 10/30/40&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;IconData&lt;/span&gt; &lt;span class="n"&gt;icon&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;labelKey&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;Widget&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;builder&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;Social contributes its tab at order 20, slotting between home and alerts without the shell knowing it exists. The shell merges its own tabs with &lt;code&gt;ModuleRegistry.shellTabs&lt;/code&gt; and sorts.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Entry points
&lt;/h3&gt;

&lt;p&gt;Home screens linked to feature screens with &lt;code&gt;Get.toNamed(...)&lt;/code&gt;. Named routes are already decoupled — no import needed — but navigating to a route that isn't registered just fails silently.&lt;/p&gt;

&lt;p&gt;So the route table answers questions about itself:&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;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;Set&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;_names&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&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;name&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;hasRoute&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;route&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;_names&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&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 UI asks before offering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppPages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRoutes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;ServiceTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;label:&lt;/span&gt; &lt;span class="s"&gt;'Cart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;onTap:&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;Get&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toNamed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRoutes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A build without the module simply doesn't show the button, instead of showing one that goes nowhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I got wrong
&lt;/h2&gt;

&lt;p&gt;Both were embarrassing, and both took one line to fix once I actually looked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A home layout imported an entire feature to format a date.&lt;/strong&gt; There was a &lt;code&gt;feedRelativeTime()&lt;/code&gt; helper living inside &lt;code&gt;feed_tab.dart&lt;/code&gt;, and the home screen used it. That single import made the social feature non-removable. Moving the helper to &lt;code&gt;shared/&lt;/code&gt; removed the dependency completely — it was never real coupling, just misplaced code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Another layout imported a map controller for two numbers.&lt;/strong&gt; A &lt;code&gt;static const&lt;/code&gt; default latitude and longitude. Same fix.&lt;/p&gt;

&lt;p&gt;If you try this on your own codebase, look for these first. A surprising amount of "architectural coupling" is just something sitting in the wrong file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigation is not the same as capability
&lt;/h2&gt;

&lt;p&gt;The subtler bug came later. My checkout screen offered "pay with wallet" unconditionally.&lt;/p&gt;

&lt;p&gt;In a build without the wallet module, that option still &lt;em&gt;worked&lt;/em&gt; — the service was still registered in &lt;code&gt;main()&lt;/code&gt;, the balance was real. There was just no wallet screen to open. A payment method with nowhere to top up.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hasRoute&lt;/code&gt; was the wrong question. It answers "can I navigate there?" What I needed was "is this feature in the build?":&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;ModuleRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'wallet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// is this FEATURE here?&lt;/span&gt;
&lt;span class="n"&gt;AppPages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRoutes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// can I navigate there?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conflating those two is exactly how you ship a payment option you can't support.&lt;/p&gt;

&lt;p&gt;This is also why I stopped trying to move shared services into modules. &lt;code&gt;WalletService&lt;/code&gt; and &lt;code&gt;CartService&lt;/code&gt; are used by several features and stay registered centrally — they're cheap and always resolvable. What must not leak is the &lt;em&gt;UI&lt;/em&gt; for a feature that isn't installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I know it works
&lt;/h2&gt;

&lt;p&gt;The part that made this trustworthy isn't the architecture, it's the test.&lt;/p&gt;

&lt;p&gt;I remove modules from the registry and rebuild, then assert:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the app still analyzes clean — no dangling references&lt;/li&gt;
&lt;li&gt;the route count drops by exactly the number that module owned&lt;/li&gt;
&lt;li&gt;the tab bar loses exactly the tabs it contributed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;isEnabled&lt;/code&gt; reports it absent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dropping the marketplace module takes the app from 38 routes to 30, removes the Shopping home layout from Settings entirely, and falls back to the all-in-one layout if that's what the user had saved. A storefront home screen with no store is worse than not offering it.&lt;/p&gt;

&lt;p&gt;Claims about modularity are cheap. Deleting things and watching the build stay green is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I haven't solved
&lt;/h2&gt;

&lt;p&gt;Repositories live in a shared &lt;code&gt;data/&lt;/code&gt; layer, which is what lets home screens preview any feature. That's a deliberate trade for legibility, but it means deleting a feature folder outright still needs its repository registration removed from &lt;code&gt;main()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Someone on r/FlutterDev suggested an event bus — modules fire events instead of calling each other, and a base module routes them. It's a genuinely better answer for cross-module communication. The trade-off is compile-time safety: you can't see who handles an event, or whether anyone does, and tracing a flow becomes grepping for string keys. For a codebase other people have to read quickly, I'm not sure that's the right trade. Still thinking about it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This came out of building &lt;a href="https://paragonframework.com" rel="noopener noreferrer"&gt;Paragon&lt;/a&gt;, a Flutter template. The demo is open if you want to poke at it — no signup.&lt;/em&gt;&lt;/p&gt;

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