cd /news/developer-tools/taming-70-flutter-flavors-flavorizr-… · home topics developer-tools article
[ARTICLE · art-85990] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

Taming 70 Flutter Flavors: flavorizr + Batch CI for White-Label Releases

Kamero, an AI-powered event photography platform, has industrialized its Flutter white-label release process to manage 70 flavors. The team uses flutter_flavorizr for declarative native configuration, a Dart FlavorConfig for per-flavor branding, and batch CI scripts to automate builds and store uploads, eliminating manual Xcode/Gradle edits and repetitive uploads.

read5 min views1 publishedAug 4, 2026

How we ship dozens of branded photography apps from one Flutter codebase—without drowning in manual Xcode/Gradle edits or one-off store uploads.

This is the approach we use at ** Kamero** — an AI-powered event photography platform (

At Kamero, our product is a multi-tenant event photography app. Each photography studio often needs:

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

At around 70 flavors, the naive approach dies:

android/app/build.gradle

and Xcode schemes per clientWe did not escape flavors. We industrialized them.

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

flavorizr owns native packaging. FlavorConfig owns first-paint branding in Dart. Scripts own release fan-out so humans do not click “build” seventy times.

We use flutter_flavorizr (with local customizations so we can extend processors). Flavors live as declarative config under pubspec.yaml

:

flavorizr:
  ide: "vscode"
  app:
    android:
      flavorDimensions: "app"
  flavors:
    acme_w1:
      app:
        name: "Acme Studios"
      ios:
        bundleId: com.example.app.1
        icon: assets/images/acme/logo.png
        firebase:
          config: config/acme/GoogleService-Info.plist
      android:
        applicationId: com.example.app.w1
        icon: assets/images/acme/logo.png
        firebase:
          config: config/shared/google-services.json
        customConfig:
          manifestPlaceholders: '= [deepLinkHost: "1"]'
          versionCode: 15004
          versionName: '"1-3.0.6"'
          signingConfig: signingConfigs.release

Running flavorizr generates / refreshes:

flavorizr.gradle

Flavor

enum consumed at startupNaming convention: {clientSlug}_w{tenantId}

(for example acme_w1

). The wN

suffix maps to a white-label / tenant ID used by the backend and feature gates.

manifestPlaceholders

per flavorconfig:

path per OSsigningConfig

explicit in customConfig

so Play uploads do not fail mysteriouslyAdding a client becomes a checklist, not archaeology.

Native flavor only gets you package identity and assets. UI still needs brand tokens. At boot:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // appFlavor comes from the native flavor / --flavor
  F.appFlavor = Flavor.values.firstWhere(
    (e) => e.name == appFlavor?.toLowerCase(),
  );

  flavorConfig = F.appFlavor.getFlavorConfig()!;
  // … init OAuth / Firebase for this flavor …
  runApp(const ProviderScope(child: MyApp()));
}

Each enum case maps to a seed config:

class FlavorConfig {
  String? appTitle;
  String splashImage;
  String? whiteLabelId;
  Color brandColor;
  Color? contentColor; // text/icons on brand surfaces
  String? logo;
  bool isLive;
  bool isPhoneRequiredOnSignup;
  bool isPhoneRequiredForProfileCompletion;

  bool get isWhiteLabel =>
      whiteLabelId != null && whiteLabelId != '0';
}

extension on Flavor {
  FlavorConfig? getFlavorConfig() {
    switch (this) {
      case Flavor.acme_w1:
        return FlavorConfig()
          ..appTitle = 'Acme Studios'
          ..splashImage = 'assets/images/acme/splash.png'
          ..logo = 'assets/images/acme/logo.png'
          ..whiteLabelId = '1'
          ..brandColor = const Color(0xFF3F51B5);
      // … one case per flavor …
    }
  }
}

Widgets do not hard-code one brand purple. Shared chrome reads helpers:

Color getBrandColor() =>
    whiteLabelModel?.brandColor ?? flavorConfig.brandColor;

Color getContentColor() =>
    whiteLabelModel?.effectiveContentColor ??
    flavorConfig.contentColor ??
    flavorConfig.brandColor;

After splash/welcome, we fetch the tenant profile. If the API returns a brandColor

hex, we set a small in-memory model so AppBars, buttons, and s pick up the latest palette without rebuilding the store binary.

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

Derived surfaces keep the design coherent from one seed:

Color get subtleBackground => Color.alphaBlend(
      brandColor.withAlpha((255 * 0.02).round()),
      const Color(0xFFF5F5F5),
    );

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.

flavorizr solves definition. It does not solve “build and upload 40 AABs tonight.” That is where batch scripts matter.

flavor_list_*.json

Instead of hard-coding the release set in bash, we keep a JSON registry:

{
  "flavors": [
    {
      "name": "main_w0",
      "enabled": false,
      "priority": 1,
      "package_name": "com.example.app"
    },
    {
      "name": "acme_w1",
      "enabled": true,
      "priority": 1,
      "package_name": "com.example.app.w1"
    }
  ]
}

enabled

— include in tonight’s batch (flip without editing the shell)package_name

— Android applicationId for Fastlane version bumps / uploadOnly enabled flavors run. If one flavor fails, the script continues and prints a summary at the end.

./scripts/cicd/build_and_upload_android.sh \
  --version-name "3.0.1" \
  --version-code 15020 \
  --track internal

Per enabled flavor, sequentially:

flavorizr.gradle

via Fastlane (update_version

  • package_name

)build/

, android/app/build/

, and android/.gradle/

(disk fills fast at N flavors)flutter build appbundle --release --flavor <name>

Optional: pass --flavor acme_w1

to smoke-test one client before enabling the full set.

Same idea, different store plumbing:

pubspec.yaml

version (name+code

)flutter build ipa --release --flavor <name> --build-number <code>

xcrun altool

using per-flavor App Store Connect API keys

./scripts/cicd/build_and_upload_ios.sh \
  --version-name "3.0.1" \
  --version-code 15020

enabled

in JSONsigningConfig

already in flavorizr outputbuild_logs/

This is our practical alternative to maintaining seventy separate CI jobs by hand: one pipeline shape, data-driven flavor set.

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

Humans decide which tenants ship. Machines do the repetitive build/sign/upload loop.

FlavorConfig

  • optional runtime overlay).We did not pretend seventy store apps are “one binary.” At Kamero, we accepted flavors, then made them operable:

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.

Building something similar for photographers or event platforms? Check out kamero.ai—happy to compare notes on white-label Flutter delivery.

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?

War stories welcome—especially signing mismatches and “disk filled on flavor #37.” Drop a comment, or find us at kamero.ai.

── more in #developer-tools 4 stories · sorted by recency
── more on @kamero 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/taming-70-flutter-fl…] indexed:0 read:5min 2026-08-04 ·