<?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: team kamero</title>
    <description>The latest articles on DEV Community by team kamero (@kamero).</description>
    <link>https://dev.to/kamero</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%2F4062332%2F78c96460-d362-4c9e-b6bd-a732f8bd4182.png</url>
      <title>DEV Community: team kamero</title>
      <link>https://dev.to/kamero</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kamero"/>
    <language>en</language>
    <item>
      <title>Taming 70 Flutter Flavors: flavorizr + Batch CI for White-Label Releases</title>
      <dc:creator>team kamero</dc:creator>
      <pubDate>Tue, 04 Aug 2026 11:35:24 +0000</pubDate>
      <link>https://dev.to/kamero/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases-54fl</link>
      <guid>https://dev.to/kamero/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases-54fl</guid>
      <description>&lt;p&gt;How we ship dozens of branded photography apps from one Flutter codebase—without drowning in manual Xcode/Gradle edits or one-off store uploads.&lt;/p&gt;

&lt;p&gt;This is the approach we use at &lt;strong&gt;&lt;a href="https://kamero.ai" rel="noopener noreferrer"&gt;Kamero&lt;/a&gt;&lt;/strong&gt; — an AI-powered event photography platform (&lt;a href="https://kamero.ai" rel="noopener noreferrer"&gt;kamero.ai&lt;/a&gt;) that delivers white-label guest apps for studios and photographers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: white-label means real store binaries
&lt;/h2&gt;

&lt;p&gt;At Kamero, our product is a multi-tenant event photography app. Each photography studio often needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own App Store / Play Store listing&lt;/li&gt;
&lt;li&gt;Its own bundle ID / applicationId&lt;/li&gt;
&lt;li&gt;Its own icon, splash, Firebase config, and deep-link host&lt;/li&gt;
&lt;li&gt;Its own signing key (especially on Android)&lt;/li&gt;
&lt;li&gt;A seed brand palette before any network call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not “swap a hex in JSON and ship one binary.” Store policy, OAuth clients, push configs, and client branding push you toward &lt;strong&gt;flavors&lt;/strong&gt;—one installable app per tenant.&lt;/p&gt;

&lt;p&gt;At around &lt;strong&gt;70 flavors&lt;/strong&gt;, the naive approach dies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hand-editing &lt;code&gt;android/app/build.gradle&lt;/code&gt; and Xcode schemes per client&lt;/li&gt;
&lt;li&gt;Remembering which tenants to release this week&lt;/li&gt;
&lt;li&gt;Rebuilding everything because one version bump was missed&lt;/li&gt;
&lt;li&gt;Filling the disk with 70× AAB/IPA intermediate artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We did not escape flavors. We &lt;strong&gt;industrialized&lt;/strong&gt; them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: three layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. flutter_flavorizr
   Declarative flavor defs → native projects + Dart enum

2. FlavorConfig (Dart)
   Per-flavor seed: title, splash, logo, brandColor, tenant ID
   + optional runtime color overlay from tenant profile

3. Batch CI scripts
   flavor_list_*.json → build only enabled → upload to stores
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;flavorizr&lt;/strong&gt; owns native packaging. &lt;strong&gt;FlavorConfig&lt;/strong&gt; owns first-paint branding in Dart. &lt;strong&gt;Scripts&lt;/strong&gt; own release fan-out so humans do not click “build” seventy times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: managing flavors with flutter_flavorizr
&lt;/h2&gt;

&lt;p&gt;We use &lt;a href="https://pub.dev/packages/flutter_flavorizr" rel="noopener noreferrer"&gt;flutter_flavorizr&lt;/a&gt; (with local customizations so we can extend processors). Flavors live as declarative config under &lt;code&gt;pubspec.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;flavorizr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ide&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vscode"&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;flavorDimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;
  &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;acme_w1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Studios"&lt;/span&gt;
      &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bundleId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.app.1&lt;/span&gt;
        &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;assets/images/acme/logo.png&lt;/span&gt;
        &lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/acme/GoogleService-Info.plist&lt;/span&gt;
      &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;applicationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.app.w1&lt;/span&gt;
        &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;assets/images/acme/logo.png&lt;/span&gt;
        &lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/shared/google-services.json&lt;/span&gt;
        &lt;span class="na"&gt;customConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;manifestPlaceholders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[deepLinkHost:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"1"]'&lt;/span&gt;
          &lt;span class="na"&gt;versionCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15004&lt;/span&gt;
          &lt;span class="na"&gt;versionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;"1-3.0.6"'&lt;/span&gt;
          &lt;span class="na"&gt;signingConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signingConfigs.release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running flavorizr generates / refreshes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android product flavors and &lt;code&gt;flavorizr.gradle&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;iOS schemes / build configs&lt;/li&gt;
&lt;li&gt;Per-flavor icons and Firebase plist/json wiring&lt;/li&gt;
&lt;li&gt;A Dart &lt;code&gt;Flavor&lt;/code&gt; enum consumed at startup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Naming convention:&lt;/strong&gt; &lt;code&gt;{clientSlug}_w{tenantId}&lt;/code&gt; (for example &lt;code&gt;acme_w1&lt;/code&gt;). The &lt;code&gt;wN&lt;/code&gt; suffix maps to a white-label / tenant ID used by the backend and feature gates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this works at scale
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New client:&lt;/strong&gt; add a YAML block + assets + regenerate (instead of hand-editing Gradle and Xcode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep links:&lt;/strong&gt; declare &lt;code&gt;manifestPlaceholders&lt;/code&gt; per flavor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase:&lt;/strong&gt; declare a &lt;code&gt;config:&lt;/code&gt; path per OS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing:&lt;/strong&gt; keep &lt;code&gt;signingConfig&lt;/code&gt; explicit in &lt;code&gt;customConfig&lt;/code&gt; so Play uploads do not fail mysteriously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a client becomes a checklist, not archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Dart-side FlavorConfig + runtime color overlay
&lt;/h2&gt;

&lt;p&gt;Native flavor only gets you package identity and assets. UI still needs brand tokens. At boot:&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;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&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="c1"&gt;// appFlavor comes from the native flavor / --flavor&lt;/span&gt;
  &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appFlavor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flavor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstWhere&lt;/span&gt;&lt;span class="p"&gt;(&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="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;appFlavor&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;flavorConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appFlavor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFlavorConfig&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="c1"&gt;// … init OAuth / Firebase for this flavor …&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;ProviderScope&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;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;Each enum case maps to a &lt;strong&gt;seed config&lt;/strong&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;class&lt;/span&gt; &lt;span class="nc"&gt;FlavorConfig&lt;/span&gt; &lt;span class="p"&gt;{&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;appTitle&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;splashImage&lt;/span&gt;&lt;span class="p"&gt;;&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;whiteLabelId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;brandColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;contentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// text/icons on brand surfaces&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;logo&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;isLive&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;isPhoneRequiredOnSignup&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;isPhoneRequiredForProfileCompletion&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;isWhiteLabel&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;whiteLabelId&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;whiteLabelId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;Flavor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;FlavorConfig&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;getFlavorConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&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="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Flavor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acme_w1&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FlavorConfig&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;appTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'Acme Studios'&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;splashImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'assets/images/acme/splash.png'&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'assets/images/acme/logo.png'&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whiteLabelId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'1'&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;brandColor&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;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF3F51B5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// … one case per flavor …&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;Widgets do not hard-code one brand purple. Shared chrome reads helpers:&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;Color&lt;/span&gt; &lt;span class="nf"&gt;getBrandColor&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;whiteLabelModel&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;brandColor&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="n"&gt;flavorConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;brandColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="nf"&gt;getContentColor&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;whiteLabelModel&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;effectiveContentColor&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
    &lt;span class="n"&gt;flavorConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentColor&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
    &lt;span class="n"&gt;flavorConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;brandColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Runtime overlay
&lt;/h3&gt;

&lt;p&gt;After splash/welcome, we fetch the tenant profile. If the API returns a &lt;code&gt;brandColor&lt;/code&gt; hex, we set a small in-memory model so AppBars, buttons, and loaders pick up the latest palette &lt;strong&gt;without rebuilding the store binary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Logos, splash, and app title stay flavor-seeded (store identity). Accent color can still move with the photographer’s profile.&lt;/p&gt;

&lt;p&gt;Derived surfaces keep the design coherent from one seed:&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;Color&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;subtleBackground&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;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;alphaBlend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;brandColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withAlpha&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;round&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;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFF5F5F5&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;Feature gates are mostly per white-label ID (for example, hide create-event for some tenants). Not elegant forever—but explicit and reviewable next to the flavor map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: CI scripts as the real alternative to manual releases
&lt;/h2&gt;

&lt;p&gt;flavorizr solves &lt;em&gt;definition&lt;/em&gt;. It does not solve “build and upload 40 AABs tonight.” That is where batch scripts matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Control plane: &lt;code&gt;flavor_list_*.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of hard-coding the release set in bash, we keep a JSON registry:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"flavors"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main_w0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="nl"&gt;"package_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.app"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme_w1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="nl"&gt;"package_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.app.w1"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;enabled&lt;/code&gt; — include in tonight’s batch (flip without editing the shell)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;package_name&lt;/code&gt; — Android applicationId for Fastlane version bumps / upload&lt;/li&gt;
&lt;li&gt;Separate lists for Android and iOS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only enabled flavors run. If one flavor fails, the script continues and prints a summary at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android batch script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/cicd/build_and_upload_android.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version-name&lt;/span&gt; &lt;span class="s2"&gt;"3.0.1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version-code&lt;/span&gt; 15020 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--track&lt;/span&gt; internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per enabled flavor, sequentially:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bump version in &lt;code&gt;flavorizr.gradle&lt;/code&gt; via Fastlane (&lt;code&gt;update_version&lt;/code&gt; + &lt;code&gt;package_name&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Clean &lt;code&gt;build/&lt;/code&gt;, &lt;code&gt;android/app/build/&lt;/code&gt;, and &lt;code&gt;android/.gradle/&lt;/code&gt; (disk fills fast at N flavors)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;flutter build appbundle --release --flavor &amp;lt;name&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Upload the AAB with Fastlane + a Play service account JSON&lt;/li&gt;
&lt;li&gt;Clean again after upload (success or failure)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Optional: pass &lt;code&gt;--flavor acme_w1&lt;/code&gt; to smoke-test one client before enabling the full set.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS batch script
&lt;/h3&gt;

&lt;p&gt;Same idea, different store plumbing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bump &lt;code&gt;pubspec.yaml&lt;/code&gt; version (&lt;code&gt;name+code&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Clean Flutter / iOS build dirs&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;flutter build ipa --release --flavor &amp;lt;name&amp;gt; --build-number &amp;lt;code&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extract dSYMs to a versioned folder before wipe (Crashlytics needs them)&lt;/li&gt;
&lt;li&gt;Upload via &lt;code&gt;xcrun altool&lt;/code&gt; using per-flavor App Store Connect API keys
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/cicd/build_and_upload_ios.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version-name&lt;/span&gt; &lt;span class="s2"&gt;"3.0.1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version-code&lt;/span&gt; 15020
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why scripts beat hand-rolled CI jobs for every flavor
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selective releases:&lt;/strong&gt; toggle &lt;code&gt;enabled&lt;/code&gt; in JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-flavor signing:&lt;/strong&gt; honor &lt;code&gt;signingConfig&lt;/code&gt; already in flavorizr output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk pressure:&lt;/strong&gt; aggressive clean before/after each flavor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggability:&lt;/strong&gt; timestamped logs under &lt;code&gt;build_logs/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial failure:&lt;/strong&gt; continue the matrix; summarize failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local / CI agnostic:&lt;/strong&gt; same bash on a release Mac or a runner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is our practical alternative to maintaining seventy separate CI jobs by hand: &lt;strong&gt;one pipeline shape, data-driven flavor set.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  End-to-end release flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New client
  → assets/ + Firebase config/
  → flavorizr YAML entry
  → regenerate native + Flavor enum
  → FlavorConfig case (seed colors, splash, whiteLabelId)
  → row in flavor_list_android.json / flavor_list_ios.json (enabled: false)
  → first manual / --flavor smoke build
  → enable in JSON
  → batch script with shared version-name + version-code
  → Play / App Store Connect
  → runtime profile may still refresh brandColor later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Humans decide &lt;em&gt;which&lt;/em&gt; tenants ship. Machines do the repetitive build/sign/upload loop.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Flavors are a distribution boundary&lt;/strong&gt;, not a theming API. Use them for store identity; keep UI tokens thin (&lt;code&gt;FlavorConfig&lt;/code&gt; + optional runtime overlay).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative generation (flavorizr) beats hand-edited native projects&lt;/strong&gt; once you pass roughly ten clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A JSON enable-list is the release feature flag&lt;/strong&gt; for white-label CI. Do not bury the release set inside the shell script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential builds + aggressive cleaning&lt;/strong&gt; are boring and necessary. Parallelizing 70 Flutter release builds without a disk strategy fails noisily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-flavor signing and ASC API keys&lt;/strong&gt; must be first-class in the checklist. Most “batch upload” failures are identity/signing, not Dart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime branding still helps&lt;/strong&gt;—photographers can change brand colors without waiting for another store review—while splash/icon/package stay flavor-owned.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We did not pretend seventy store apps are “one binary.” At &lt;a href="https://kamero.ai" rel="noopener noreferrer"&gt;Kamero&lt;/a&gt;, we accepted flavors, then made them operable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;flutter_flavorizr&lt;/strong&gt; for consistent native + Dart flavor scaffolding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FlavorConfig&lt;/strong&gt; for seed branding and tenant ID wiring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch CI scripts + flavor_list JSON&lt;/strong&gt; so releases are toggles and version args, not tribal knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your white-label story requires separate listings, invest in generation + batching early. The cost of flavors is not the YAML—it is the release matrix. Scripts are how we keep that matrix boring.&lt;/p&gt;

&lt;p&gt;Building something similar for photographers or event platforms? Check out &lt;a href="https://kamero.ai" rel="noopener noreferrer"&gt;kamero.ai&lt;/a&gt;—happy to compare notes on white-label Flutter delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;Do you generate flavors (flavorizr / custom codegen) or maintain native projects by hand? And for releases: one mega CI matrix, or a data-driven enable-list like ours?&lt;/p&gt;

&lt;p&gt;War stories welcome—especially signing mismatches and “disk filled on flavor #37.” Drop a comment, or find us at &lt;a href="https://kamero.ai" rel="noopener noreferrer"&gt;kamero.ai&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>whitelabel</category>
      <category>mobilearchitecture</category>
    </item>
  </channel>
</rss>
