← Articles

Automating Flutter flavor setup with a single YAML spec

By Ann Tech · 3 October 2024

Setting up Flutter flavors manually means editing build.gradle.kts, Info.plist, Xcode schemes, and Dart entry points — four different files for every flavor you add. With annspec.yaml and ann_flutter_flavor, you write the spec once and generate everything.

The problem with manual flavor setup

Here is what a manual three-flavor setup requires:

  • Android: flavor dimensions and product flavors in build.gradle.kts, one google-services.json per flavor source set, signing config per flavor
  • iOS: one Xcode scheme per flavor, one build configuration per scheme, bundle identifier overrides in each configuration
  • Dart: one entry point file per flavor (main_dev.dart, main_staging.dart, main_production.dart)
  • Fastlane: one lane per flavor

Every time you add a flavor or change a bundle ID, you touch all of these. With ann_flutter_flavor, you touch one file.

The annspec.yaml spec

# annspec.yaml — at the Flutter project root
app:
  name: Ledger
  organizationId: com.annai

flavors:
  dev:
    appName: Ledger Dev
    bundleId: com.annai.ledger.dev
    apiBaseUrl: https://dev.api.annaibrands.com
    firebase:
      androidAppId: "1:111:android:abc"
      iosAppId: "1:111:ios:abc"
    android:
      signingConfig: debug
    ios:
      teamId: ABCDE12345

  staging:
    appName: Ledger Staging
    bundleId: com.annai.ledger.staging
    apiBaseUrl: https://staging.api.annaibrands.com
    firebase:
      androidAppId: "1:222:android:def"
      iosAppId: "1:222:ios:def"
    android:
      signingConfig: staging
    ios:
      teamId: ABCDE12345

  production:
    appName: Ledger
    bundleId: com.annai.ledger
    apiBaseUrl: https://api.annaibrands.com
    firebase:
      androidAppId: "1:333:android:ghi"
      iosAppId: "1:333:ios:ghi"
    android:
      signingConfig: release
    ios:
      teamId: ABCDE12345

Running the sync command

dart run ann_flutter_flavor sync

This generates:

  • android/app/build.gradle.kts flavor blocks
  • ios/Runner.xcodeproj schemes and configurations
  • lib/generated/ann_flavor.g.dart — typed Dart access to flavor values
  • Firebase initialization scripts
  • Fastlane lane stubs

Using generated flavor constants in Dart

After running sync, access flavor values from the generated file:

import 'package:my_app/generated/ann_flavor.g.dart';

void main() {
  runApp(MyApp(
    apiBaseUrl: AnnFlavor.current.apiBaseUrl,
    appName: AnnFlavor.current.appName,
  ));
}

The generated AnnFlavor.current reads the compile-time FLAVOR constant set by Flutter's --flavor flag.

What the generated Android config looks like

// android/app/build.gradle.kts (generated section)
android {
    flavorDimensions += "env"
    productFlavors {
        create("dev") {
            dimension = "env"
            applicationId = "com.annai.ledger.dev"
            resValue("string", "app_name", "Ledger Dev")
        }
        create("staging") {
            dimension = "env"
            applicationId = "com.annai.ledger.staging"
            resValue("string", "app_name", "Ledger Staging")
        }
        create("production") {
            dimension = "env"
            applicationId = "com.annai.ledger"
            resValue("string", "app_name", "Ledger")
        }
    }
}

Building a specific flavor

# Run dev
flutter run --flavor dev -t lib/main.dart

# Build staging APK
flutter build apk --flavor staging -t lib/main.dart --release

# Build production AAB
flutter build appbundle --flavor production -t lib/main.dart --release

# iOS
flutter build ios --flavor production -t lib/main.dart --release

Re-running after changes

Whenever you change annspec.yaml, re-run sync:

dart run ann_flutter_flavor sync

Commit both annspec.yaml and the generated files. The generated files are deterministic — they're safe to commit and regeneration is idempotent.

Adding the sync step to CI

# .github/workflows/build.yml
- name: Sync flavor config
  run: dart run ann_flutter_flavor sync

- name: Build production
  run: flutter build appbundle --flavor production --release

Validating the spec

dart run ann_flutter_flavor validate

This checks for common mistakes: duplicate bundle IDs, missing required fields, invalid team IDs.

Common pitfalls

Not re-running sync after editing annspec.yaml. The generated files become stale. Add a pre-build hook or CI step to always run sync before building.

Flavor names with hyphens or spaces. Android product flavor names must be valid Java identifiers. Use underscores or camelCase: staging_v2, not staging-v2.

Mismatched bundle IDs between iOS and Android. It's easy for them to drift when edited manually. The YAML spec keeps them in one place — change once, applied everywhere.

Forgetting to add new Firebase App IDs. When you add a new Firebase project, update the firebase.androidAppId and firebase.iosAppId in the spec and re-sync. The Gradle plugin reads these to configure Firebase initialization.

Sign in to like, dislike, or report.

Automating Flutter flavor setup with a single YAML spec — ANN Tech