<?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>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>
