Flavors, Signing, Release Builds & Store Workflows
Managing dev/staging/prod environments, Android signing, and app store submission pipelines
Open interactive version (quiz + challenge)Real-world analogy
Build flavors are like a restaurant having separate menus for staff meals, soft openings, and the public — same kitchen, different ingredients and branding. Signing is the restaurant's health certificate — the store will not let you serve food without it.
What is it?
Build flavors enable managing multiple app environments (dev/staging/prod) from one codebase. Android signing with keystores and iOS provisioning profiles are mandatory for release builds. Understanding the full store submission pipeline is a key senior Flutter engineer competency.
Real-world relevance
On a fintech claims app, the team ran BankID auth testing against the production BankID endpoint from the dev flavor because the API_BASE_URL was hardcoded. After setting up flavors with dart-define, dev flavor pointed to the sandbox, preventing real BankID calls in development.
Key points
- What build flavors are — Flavors let you build multiple app variants from the same codebase with different bundle IDs, API endpoints, app names, icons, and Firebase projects. Common flavors: development, staging, production.
- Android flavor setup — Define productFlavors in android/app/build.gradle. Each flavor can override applicationId, versionName, and resValues. Flutter accesses the current flavor via --flavor flag at build time.
- iOS flavor setup — iOS uses Schemes and Targets instead of flavors. Create a scheme per environment in Xcode. The --flavor flag maps to a scheme name. More complex than Android — a common source of iOS CI failures.
- Dart-side environment config — Use a FlavorConfig class or dart-define to inject environment values (API URLs, feature flags) at compile time. Avoid reading from .env files at runtime in production — values should be baked in at build time.
- Android signing — keystore — Generate a keystore with keytool. Store signing config in android/key.properties (gitignored). Reference key.properties in build.gradle using signingConfigs. Never commit the keystore or key.properties to git.
- Release build commands — flutter build apk --release --flavor production builds a signed split APK. flutter build appbundle --release --flavor production builds an AAB for Play Store. AAB is required for new Play Store submissions.
- Play Store submission workflow — Upload AAB to Internal Testing track → promote to Closed Testing (beta) → promote to Production. Each promotion can be gated on crashlytics thresholds and review. Use fastlane or Codemagic for automation.
- iOS provisioning and signing — Requires Apple Developer account, provisioning profiles (App Store distribution), and distribution certificate. match (fastlane) manages these in a git repo. Manual management is error-prone and does not scale.
- Versioning strategy — pubspec.yaml version: 1.2.3+45 — the 1.2.3 is the display version (major.minor.patch), 45 is the build number (versionCode on Android, CFBundleVersion on iOS). Build number must increment with every store submission.
- Over-the-air updates — Shorebird provides Dart-level code push — deploy bug fixes without going through app store review. Awareness of this in an interview signals production maturity. Not a replacement for proper releases but valuable for critical bug fixes.
Code example
// === android/app/build.gradle — flavor configuration ===
android {
flavorDimensions "environment"
productFlavors {
development {
dimension "environment"
applicationId "com.example.app.dev"
resValue "string", "app_name", "MyApp Dev"
versionNameSuffix "-dev"
}
staging {
dimension "environment"
applicationId "com.example.app.staging"
resValue "string", "app_name", "MyApp Staging"
}
production {
dimension "environment"
applicationId "com.example.app"
resValue "string", "app_name", "MyApp"
}
}
signingConfigs {
release {
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('app/key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
}
}
}
// === lib/config/flavor_config.dart ===
enum Flavor { development, staging, production }
class FlavorConfig {
final Flavor flavor;
final String apiBaseUrl;
final bool enableCrashlytics;
static late FlavorConfig _instance;
static FlavorConfig get instance => _instance;
FlavorConfig._({
required this.flavor,
required this.apiBaseUrl,
required this.enableCrashlytics,
});
static void initialize({
required Flavor flavor,
required String apiBaseUrl,
required bool enableCrashlytics,
}) {
_instance = FlavorConfig._(
flavor: flavor,
apiBaseUrl: apiBaseUrl,
enableCrashlytics: enableCrashlytics,
);
}
}
// === main_production.dart ===
void main() {
FlavorConfig.initialize(
flavor: Flavor.production,
apiBaseUrl: 'https://api.example.com',
enableCrashlytics: true,
);
runApp(const MyApp());
}
// === Build commands ===
// flutter run --flavor development -t lib/main_development.dart
// flutter build appbundle --release --flavor production -t lib/main_production.dartLine-by-line walkthrough
- 1. productFlavors { development { applicationId 'com.example.app.dev' } } — different bundle ID per flavor lets dev and prod be installed side by side on the same device
- 2. resValue 'string', 'app_name', 'MyApp Dev' — overrides the app name shown on the home screen per flavor
- 3. signingConfigs.release reads from key.properties — the file is gitignored; CI injects it from secrets at build time
- 4. minifyEnabled true — enables R8 code shrinking and obfuscation in release builds
- 5. class FlavorConfig — singleton pattern with static late _instance; initialised in main_production.dart before runApp
- 6. main_production.dart sets apiBaseUrl to the real API — each flavor has its own main file as the entry point
- 7. flutter build appbundle --release --flavor production -t lib/main_production.dart — specifies both the flavor and entry point
Spot the bug
// android/app/build.gradle
signingConfigs {
release {
keyAlias 'my-key'
keyPassword 'supersecret123'
storeFile file('keystore.jks')
storePassword 'keystorepass456'
}
}Need a hint?
This will build successfully but creates a serious security problem.
Show answer
Bug: The signing credentials (keyAlias, keyPassword, storeFile, storePassword) are hardcoded directly in build.gradle. If this file is committed to git (which it usually is), the keystore password is exposed to everyone with repo access — and permanently in git history even if later removed. Fix: move credentials to android/key.properties (add to .gitignore), load them with Properties() at build time, and store the actual values as CI secrets injected at build time.
Explain like I'm 5
Imagine your app is a pizza. Flavors let you make a 'development pizza' (cheap ingredients, messy, for the chefs to taste), a 'staging pizza' (almost the real thing, for trusted friends to review), and a 'production pizza' (perfect, for paying customers). Signing is the health certificate that says the pizza was made in a licensed kitchen — the app store refuses unsigned pizzas.
Fun fact
The Android App Bundle (AAB) format, required for Play Store since August 2021, allows Google Play to generate optimised APKs per device configuration. This typically reduces app download size by 15-20% compared to a universal APK.
Hands-on challenge
Describe (in interview-ready language) how you would set up three flavors for a fintech app: dev (sandbox API, debug signing), staging (staging API, debug signing), production (production API, release signing). Include how you would prevent the keystore from being committed to git.
More resources
- Flutter Build Flavors (Flutter Docs)
- Android App Signing (Android Docs)
- Flutter Android Release Build (Flutter Docs)
- Shorebird Code Push (Shorebird)