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, onegoogle-services.jsonper 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.ktsflavor blocksios/Runner.xcodeprojschemes and configurationslib/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.