{"slug": "taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases", "title": "Taming 70 Flutter Flavors: flavorizr + Batch CI for White-Label Releases", "summary": "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.", "body_md": "How we ship dozens of branded photography apps from one Flutter codebase—without drowning in manual Xcode/Gradle edits or one-off store uploads.\n\nThis is the approach we use at ** Kamero** — an AI-powered event photography platform (\n\nAt Kamero, our product is a multi-tenant event photography app. Each photography studio often needs:\n\nThat 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.\n\nAt around **70 flavors**, the naive approach dies:\n\n`android/app/build.gradle`\n\nand Xcode schemes per clientWe did not escape flavors. We **industrialized** them.\n\n```\n1. flutter_flavorizr\n   Declarative flavor defs → native projects + Dart enum\n\n2. FlavorConfig (Dart)\n   Per-flavor seed: title, splash, logo, brandColor, tenant ID\n   + optional runtime color overlay from tenant profile\n\n3. Batch CI scripts\n   flavor_list_*.json → build only enabled → upload to stores\n```\n\n**flavorizr** owns native packaging. **FlavorConfig** owns first-paint branding in Dart. **Scripts** own release fan-out so humans do not click “build” seventy times.\n\nWe use [flutter_flavorizr](https://pub.dev/packages/flutter_flavorizr) (with local customizations so we can extend processors). Flavors live as declarative config under `pubspec.yaml`\n\n:\n\n```\nflavorizr:\n  ide: \"vscode\"\n  app:\n    android:\n      flavorDimensions: \"app\"\n  flavors:\n    acme_w1:\n      app:\n        name: \"Acme Studios\"\n      ios:\n        bundleId: com.example.app.1\n        icon: assets/images/acme/logo.png\n        firebase:\n          config: config/acme/GoogleService-Info.plist\n      android:\n        applicationId: com.example.app.w1\n        icon: assets/images/acme/logo.png\n        firebase:\n          config: config/shared/google-services.json\n        customConfig:\n          manifestPlaceholders: '= [deepLinkHost: \"1\"]'\n          versionCode: 15004\n          versionName: '\"1-3.0.6\"'\n          signingConfig: signingConfigs.release\n```\n\nRunning flavorizr generates / refreshes:\n\n`flavorizr.gradle`\n\n`Flavor`\n\nenum consumed at startup**Naming convention:** `{clientSlug}_w{tenantId}`\n\n(for example `acme_w1`\n\n). The `wN`\n\nsuffix maps to a white-label / tenant ID used by the backend and feature gates.\n\n`manifestPlaceholders`\n\nper flavor`config:`\n\npath per OS`signingConfig`\n\nexplicit in `customConfig`\n\nso Play uploads do not fail mysteriouslyAdding a client becomes a checklist, not archaeology.\n\nNative flavor only gets you package identity and assets. UI still needs brand tokens. At boot:\n\n```\nFuture<void> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  // appFlavor comes from the native flavor / --flavor\n  F.appFlavor = Flavor.values.firstWhere(\n    (e) => e.name == appFlavor?.toLowerCase(),\n  );\n\n  flavorConfig = F.appFlavor.getFlavorConfig()!;\n  // … init OAuth / Firebase for this flavor …\n  runApp(const ProviderScope(child: MyApp()));\n}\n```\n\nEach enum case maps to a **seed config**:\n\n```\nclass FlavorConfig {\n  String? appTitle;\n  String splashImage;\n  String? whiteLabelId;\n  Color brandColor;\n  Color? contentColor; // text/icons on brand surfaces\n  String? logo;\n  bool isLive;\n  bool isPhoneRequiredOnSignup;\n  bool isPhoneRequiredForProfileCompletion;\n\n  bool get isWhiteLabel =>\n      whiteLabelId != null && whiteLabelId != '0';\n}\n\nextension on Flavor {\n  FlavorConfig? getFlavorConfig() {\n    switch (this) {\n      case Flavor.acme_w1:\n        return FlavorConfig()\n          ..appTitle = 'Acme Studios'\n          ..splashImage = 'assets/images/acme/splash.png'\n          ..logo = 'assets/images/acme/logo.png'\n          ..whiteLabelId = '1'\n          ..brandColor = const Color(0xFF3F51B5);\n      // … one case per flavor …\n    }\n  }\n}\n```\n\nWidgets do not hard-code one brand purple. Shared chrome reads helpers:\n\n``` js\nColor getBrandColor() =>\n    whiteLabelModel?.brandColor ?? flavorConfig.brandColor;\n\nColor getContentColor() =>\n    whiteLabelModel?.effectiveContentColor ??\n    flavorConfig.contentColor ??\n    flavorConfig.brandColor;\n```\n\nAfter splash/welcome, we fetch the tenant profile. If the API returns a `brandColor`\n\nhex, we set a small in-memory model so AppBars, buttons, and loaders pick up the latest palette **without rebuilding the store binary**.\n\nLogos, splash, and app title stay flavor-seeded (store identity). Accent color can still move with the photographer’s profile.\n\nDerived surfaces keep the design coherent from one seed:\n\n``` js\nColor get subtleBackground => Color.alphaBlend(\n      brandColor.withAlpha((255 * 0.02).round()),\n      const Color(0xFFF5F5F5),\n    );\n```\n\nFeature 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.\n\nflavorizr solves *definition*. It does not solve “build and upload 40 AABs tonight.” That is where batch scripts matter.\n\n`flavor_list_*.json`\n\nInstead of hard-coding the release set in bash, we keep a JSON registry:\n\n```\n{\n  \"flavors\": [\n    {\n      \"name\": \"main_w0\",\n      \"enabled\": false,\n      \"priority\": 1,\n      \"package_name\": \"com.example.app\"\n    },\n    {\n      \"name\": \"acme_w1\",\n      \"enabled\": true,\n      \"priority\": 1,\n      \"package_name\": \"com.example.app.w1\"\n    }\n  ]\n}\n```\n\n`enabled`\n\n— include in tonight’s batch (flip without editing the shell)`package_name`\n\n— Android applicationId for Fastlane version bumps / uploadOnly enabled flavors run. If one flavor fails, the script continues and prints a summary at the end.\n\n```\n./scripts/cicd/build_and_upload_android.sh \\\n  --version-name \"3.0.1\" \\\n  --version-code 15020 \\\n  --track internal\n```\n\nPer enabled flavor, sequentially:\n\n`flavorizr.gradle`\n\nvia Fastlane (`update_version`\n\n+ `package_name`\n\n)`build/`\n\n, `android/app/build/`\n\n, and `android/.gradle/`\n\n(disk fills fast at N flavors)`flutter build appbundle --release --flavor <name>`\n\nOptional: pass `--flavor acme_w1`\n\nto smoke-test one client before enabling the full set.\n\nSame idea, different store plumbing:\n\n`pubspec.yaml`\n\nversion (`name+code`\n\n)`flutter build ipa --release --flavor <name> --build-number <code>`\n\n`xcrun altool`\n\nusing per-flavor App Store Connect API keys\n\n```\n./scripts/cicd/build_and_upload_ios.sh \\\n  --version-name \"3.0.1\" \\\n  --version-code 15020\n```\n\n`enabled`\n\nin JSON`signingConfig`\n\nalready in flavorizr output`build_logs/`\n\nThis is our practical alternative to maintaining seventy separate CI jobs by hand: **one pipeline shape, data-driven flavor set.**\n\n```\nNew client\n  → assets/ + Firebase config/\n  → flavorizr YAML entry\n  → regenerate native + Flavor enum\n  → FlavorConfig case (seed colors, splash, whiteLabelId)\n  → row in flavor_list_android.json / flavor_list_ios.json (enabled: false)\n  → first manual / --flavor smoke build\n  → enable in JSON\n  → batch script with shared version-name + version-code\n  → Play / App Store Connect\n  → runtime profile may still refresh brandColor later\n```\n\nHumans decide *which* tenants ship. Machines do the repetitive build/sign/upload loop.\n\n`FlavorConfig`\n\n+ optional runtime overlay).We did not pretend seventy store apps are “one binary.” At [Kamero](https://kamero.ai), we accepted flavors, then made them operable:\n\nIf 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.\n\nBuilding something similar for photographers or event platforms? Check out [kamero.ai](https://kamero.ai)—happy to compare notes on white-label Flutter delivery.\n\nDo 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?\n\nWar stories welcome—especially signing mismatches and “disk filled on flavor #37.” Drop a comment, or find us at [kamero.ai](https://kamero.ai).", "url": "https://wpnews.pro/news/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases", "canonical_source": "https://dev.to/kamero/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases-54fl", "published_at": "2026-08-04 11:35:24+00:00", "updated_at": "2026-08-04 11:49:12.029359+00:00", "lang": "en", "topics": ["developer-tools", "mlops"], "entities": ["Kamero", "Flutter", "flutter_flavorizr", "Xcode", "Gradle"], "alternates": {"html": "https://wpnews.pro/news/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases", "markdown": "https://wpnews.pro/news/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases.md", "text": "https://wpnews.pro/news/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases.txt", "jsonld": "https://wpnews.pro/news/taming-70-flutter-flavors-flavorizr-batch-ci-for-white-label-releases.jsonld"}}