<?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>Forem: Aakash Gupta</title>
    <description>The latest articles on Forem by Aakash Gupta (@akash_gupta_1998).</description>
    <link>https://forem.com/akash_gupta_1998</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3884003%2F4380b41a-9eec-44d0-8f6c-aa6ced7d0da0.jpg</url>
      <title>Forem: Aakash Gupta</title>
      <link>https://forem.com/akash_gupta_1998</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/akash_gupta_1998"/>
    <language>en</language>
    <item>
      <title>Our Home Screen Renders 12 Components — Without Knowing Any of Them</title>
      <dc:creator>Aakash Gupta</dc:creator>
      <pubDate>Fri, 17 Apr 2026 08:38:45 +0000</pubDate>
      <link>https://forem.com/akash_gupta_1998/our-home-screen-renders-12-components-without-knowing-any-of-them-31k8</link>
      <guid>https://forem.com/akash_gupta_1998/our-home-screen-renders-12-components-without-knowing-any-of-them-31k8</guid>
      <description>&lt;p&gt;The Widget Factory pattern that lets our CMS control what 2M+ users see — without a single app release.&lt;/p&gt;




&lt;p&gt;Our home screen serves millions of page views per month. It renders carousels, banners, sign-in cards, feeds, grids, and more.&lt;/p&gt;

&lt;p&gt;The screen has no idea what it's rendering.&lt;/p&gt;

&lt;p&gt;A content team in a CMS dashboard decides what appears, in what order, for which users — and the app renders it. No code change. No app release. No PR review. Here's the architecture that makes that possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Every Home Screen Change Required a Release
&lt;/h2&gt;

&lt;p&gt;Twelve months ago, our home screen was a &lt;code&gt;Column&lt;/code&gt; with hardcoded widgets. Product carousel at the top, banner in the middle, categories at the bottom. Always. For everyone.&lt;/p&gt;

&lt;p&gt;It wasn't completely static — data came from the CMS — but the &lt;strong&gt;layout was frozen in code&lt;/strong&gt;. The feed widget alone was 350+ lines of nested &lt;code&gt;if&lt;/code&gt; blocks:&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;banners&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;shouldShowBannerCarousel&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;shouldShowMosaicGrid&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;isGuestUser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;shouldShowSecondaryMosaic&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;ugcPosition&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;FeedPosition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;top&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;categories&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;// and on and on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was even a TODO buried in the file: &lt;em&gt;"Create a method that returns home feed widgets dynamically."&lt;/em&gt; We'd been telling ourselves to fix it for months.&lt;/p&gt;

&lt;p&gt;Want to swap the banner and carousel? Code change. Want to show a sign-in card only to guest users? Another &lt;code&gt;if&lt;/code&gt; block. Want to A/B test a new component position? Two code changes and a feature flag.&lt;/p&gt;

&lt;p&gt;The content team had ideas faster than we could ship them. We were the bottleneck — and every "quick layout tweak" took a sprint.&lt;/p&gt;

&lt;p&gt;My colleague Dip and I got the assignment: make the home screen &lt;strong&gt;data-driven&lt;/strong&gt;. The CMS should define &lt;em&gt;what&lt;/em&gt; appears and &lt;em&gt;where&lt;/em&gt;. The app should just render it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Four Layers, One Factory
&lt;/h2&gt;

&lt;p&gt;Here's the data flow Dip and I designed, from CMS to screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqr53bakdarn87xiu260.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqr53bakdarn87xiu260.png" alt="Flow diagram showing CMS-driven UI rendering: Contentful CMS → GraphQL API → DTO layer → domain entities → BLoC → ListView → widget factory → dynamically rendered UI components." width="800" height="862"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The critical insight: &lt;strong&gt;the &lt;code&gt;ListView&lt;/code&gt; doesn't care what types exist&lt;/strong&gt;. It iterates a list and delegates every item to a &lt;code&gt;WidgetFactory&lt;/code&gt;. The factory resolves &lt;code&gt;"ProductCarousel"&lt;/code&gt; → &lt;code&gt;CarouselWidgetBuilder&lt;/code&gt; → &lt;code&gt;CarouselWidget&lt;/code&gt;. The screen never imports a single component.&lt;/p&gt;

&lt;p&gt;Think of it as: &lt;strong&gt;CMS sends JSON → app turns it into widgets.&lt;/strong&gt; Everything in between is just type safety and caching.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Widget Factory: O(1) Type Resolution
&lt;/h2&gt;

&lt;p&gt;The factory is a singleton with a pre-built lookup map. On init, it iterates an enum of supported types and caches each builder:&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;WidgetFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;WidgetFactory&lt;/span&gt; &lt;span class="n"&gt;_instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WidgetFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;WidgetFactory&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;instance&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;_instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;Map&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;,&lt;/span&gt; &lt;span class="n"&gt;IWidgetBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_builderCache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;WidgetFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_builderCache&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;model&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SupportedComponentType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&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="na"&gt;cmsId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;widgetBuilder&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="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;createWidget&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;LayoutWidgetEntity&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;final&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_builderCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentModelType&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;builder&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;canHandle&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="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shrink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Unknown type? Skip silently.&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;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;switch&lt;/code&gt; statement. No &lt;code&gt;if-else&lt;/code&gt; chain. A &lt;code&gt;Map&lt;/code&gt; lookup — &lt;code&gt;O(1)&lt;/code&gt; regardless of how many component types we add. Unrecognized types from the CMS render as empty space, not crashes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjqez8x8u2879b9xjh23.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjqez8x8u2879b9xjh23.png" alt="Flow of widget rendering" width="800" height="144"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Registry: One Enum to Rule Them All
&lt;/h2&gt;

&lt;p&gt;Every supported component type lives in a single enum. Each entry maps a CMS string to a concrete builder:&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;enum&lt;/span&gt; &lt;span class="n"&gt;SupportedComponentType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;productCarousel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;signInCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;contentBanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;userContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;contextualMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;infoBanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;recentActivity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mosaicGrid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;promotionBanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;categoryCarousel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...12 types total&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;cmsId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;productCarousel&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;'ProductCarousel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;signInCard&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;'SignInCard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;contentBanner&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;'ContentBanner'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="n"&gt;IWidgetBuilder&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;widgetBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;productCarousel&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;CarouselWidgetBuilder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;signInCard&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;SignInCardWidgetBuilder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adding a new component type = one enum entry + one builder class.&lt;/strong&gt; The factory, the screen, the cache, the BLoC — none of them change. Dip and I have watched teammates add a new component in under two hours. Most of that time was writing the widget itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Each Component is an Island
&lt;/h2&gt;

&lt;p&gt;Every builder wraps its widget in its own &lt;code&gt;BlocProvider&lt;/code&gt;. The product carousel gets a &lt;code&gt;CarouselBloc&lt;/code&gt;. The announcement banner gets an &lt;code&gt;AnnouncementBloc&lt;/code&gt;. They share nothing.&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;CarouselWidgetBuilder&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;IWidgetBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;canHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LayoutWidgetEntity&lt;/span&gt; &lt;span class="n"&gt;layout&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;layout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentModelType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'ProductCarousel'&lt;/span&gt; 
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sysId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&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="n"&gt;LayoutWidgetEntity&lt;/span&gt; &lt;span class="n"&gt;entity&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;MultiBlocProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;providers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;BlocProvider&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;getIt&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CarouselBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()),&lt;/span&gt;
        &lt;span class="n"&gt;BlocProvider&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;getIt&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CarouselItemBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;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;CarouselWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;widgetEntity:&lt;/span&gt; &lt;span class="n"&gt;entity&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;This isolation didn't happen on day one. We initially tried sharing BLoCs between components — it broke within a week. One component's loading state leaked into another's UI. That's when Dip and I enforced full isolation: if you're a component, you bring your own state management.&lt;/p&gt;




&lt;h2&gt;
  
  
  Polymorphic Data: Same Widget, Seven Sources
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised both of us. The same &lt;code&gt;ProductCarousel&lt;/code&gt; component renders data from &lt;strong&gt;seven different backends&lt;/strong&gt; — same UI, different data sources selected via CMS, zero code changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data Source&lt;/th&gt;
&lt;th&gt;What It Shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CMS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Editorially curated products&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PersonalizationBFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Personalized for you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NewArrivalsBFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New products&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RepurchaseBFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Previously purchased&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TrendingBFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trending items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ComplementaryBFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Goes well with..."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CategoryBFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Category-based suggestions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;CarouselBloc&lt;/code&gt; reads &lt;code&gt;entity.dataSourceType&lt;/code&gt; and calls the right repository. The widget doesn't know or care where its products came from. The content team picks the data source in the CMS dropdown — the same carousel component renders personalized recommendations or editorial picks depending on configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audience Filtering: The Invisible Gatekeeper
&lt;/h2&gt;

&lt;p&gt;Not every user should see every component. Guest users see a sign-in card. Loyalty members see exclusive offers. The CMS attaches audience rules to each component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audienceFields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AudienceRules&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;premiumMember&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;loyaltyMember&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;dualMembership&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;loggedInUser&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;guestUser&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;During DTO→Entity conversion, we evaluate these rules against the current user's state. Dip built the audience resolution logic — clean, single-pass, no widget-level branching. If the user doesn't match, the entity's &lt;code&gt;isVisible&lt;/code&gt; flag is set to &lt;code&gt;false&lt;/code&gt;. The &lt;code&gt;ListView&lt;/code&gt; checks this flag before rendering:&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="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="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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layout&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isVisible&lt;/span&gt;
      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;WidgetFactory&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;createWidget&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;widget&lt;/span&gt;&lt;span class="p"&gt;)&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;SizedBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shrink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filtering happens &lt;strong&gt;once&lt;/strong&gt;, at the data boundary — not in each component, not in the UI layer, not scattered across widgets. One place.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Preloading.&lt;/strong&gt; Right now, each component fetches its own data when it scrolls into view. A component at position 8 doesn't start loading until the user scrolls there. We'd add viewport-aware preloading — start fetching components 2-3 positions ahead of the current scroll position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error boundaries per component.&lt;/strong&gt; We handle errors gracefully, but a bad API response from one data source can still show an empty slot. We'd wrap each builder output in an error boundary widget that shows a minimal fallback instead of empty space.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Let the CMS define layout, not your code.&lt;/strong&gt; If your home screen changes require app releases, you've coupled content to code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Factory + Registry &amp;gt; switch statements.&lt;/strong&gt; An enum-driven registry scales to 50 component types without touching the factory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate components with their own BLoCs.&lt;/strong&gt; No shared state, no cascade failures, parallel development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter at the data boundary, not the UI.&lt;/strong&gt; Audience rules belong in the DTO→Entity conversion, not scattered across widgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache aggressively, refresh silently.&lt;/strong&gt; Users don't care about freshness on a 200ms cold start. They care about seeing &lt;em&gt;something&lt;/em&gt; immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our home screen went from "every change is a sprint" to "the content team ships layout changes before Dip and I finish our coffee."&lt;/p&gt;

&lt;p&gt;The app doesn't know what components exist. And that's exactly when you know you've finally decoupled your UI.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;Flutter&lt;/code&gt;, &lt;code&gt;Mobile Development&lt;/code&gt;, &lt;code&gt;Software Architecture&lt;/code&gt;, &lt;code&gt;Clean Architecture&lt;/code&gt;, &lt;code&gt;CMS&lt;/code&gt;&lt;/p&gt;




</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>mobile</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
