prprompts-flutter-generator
Version:
AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp
1,853 lines (1,415 loc) • 47.5 kB
Markdown
# Flutter Flavors Setup Skill - Documentation
> **Complete guide to Flutter flavors** - Multi-level documentation for junior, intermediate, and senior developers
## Table of Contents
- [For Juniors: Understanding Flutter Flavors](#for-juniors-understanding-flutter-flavors)
- [For Intermediates: Technical Implementation](#for-intermediates-technical-implementation)
- [For Seniors: Advanced Patterns & Best Practices](#for-seniors-advanced-patterns--best-practices)
- [Quick Reference](#quick-reference)
# For Juniors: Understanding Flutter Flavors
## What Are Flutter Flavors? (ELI5)
**Simple Analogy:**
Imagine you're running a restaurant 🍽️. You have three different versions of your menu:
1. **Kitchen Test Menu** (Dev) - You test new recipes here
2. **Staff Preview Menu** (Staging) - Your staff tries the dishes before customers
3. **Customer Menu** (Production) - The final menu customers see
Flutter flavors are like having **three copies of your app**:
- **Dev** - For testing while you code
- **Staging** - For final testing before release
- **Production** - The real app users download
## Why Do We Need Flavors?
### Problem Without Flavors:
You're building an app that talks to a server. You have:
- A **test server** at `https://test-api.example.com` (for development)
- A **real server** at `https://api.example.com` (for users)
**Without flavors**, you have to:
1. Change the API URL in your code manually
2. Build the app
3. Test it
4. Change the API URL back to production
5. Build again for release
6. **DANGER:** Accidentally release with test URL! 😱
**With flavors**:
1. Build **dev flavor** → automatically uses test API
2. Build **production flavor** → automatically uses real API
3. **Never mix them up!** ✅
## What Flavors Give You
### 1. Different API Endpoints
```dart
// Dev flavor automatically uses:
apiUrl = "https://dev-api.example.com"
// Production flavor automatically uses:
apiUrl = "https://api.example.com"
```
### 2. Different App Names
On your phone, you see:
- 📱 **"MyApp Dev"** - Development version
- 📱 **"MyApp Staging"** - Staging version
- 📱 **"MyApp"** - Production version
**You can have all 3 installed at once!** They don't overwrite each other.
### 3. Different App Icons (Optional)
- Dev: Green icon with "DEV" badge
- Staging: Orange icon with "STAGING" badge
- Production: Blue icon (final design)
### 4. Different Settings
```dart
// Dev flavor:
enableAnalytics = false // Don't track test data
showDebugInfo = true // Show extra debug info
// Production flavor:
enableAnalytics = true // Track real user data
showDebugInfo = false // Hide debug info
```
## How to Use Flavors (Step by Step)
### Step 1: Run a Specific Flavor
```bash
# Run development flavor
flutter run --flavor dev -t lib/main_dev.dart
# Run staging flavor
flutter run --flavor staging -t lib/main_staging.dart
# Run production flavor
flutter run --flavor production -t lib/main_production.dart
```
**What this does:**
- `--flavor dev` tells Flutter which flavor to build
- `-t lib/main_dev.dart` tells Flutter which entry point file to use
### Step 2: Build Release APK
```bash
# Build production release
flutter build apk --flavor production -t lib/main_production.dart --release
```
**Output:** `app-production-release.apk` (ready for Play Store)
## Understanding the Files
After setting up flavors, you'll see these files:
### Entry Point Files (lib/)
```
lib/
├── main_dev.dart ← Dev flavor entry point
├── main_staging.dart ← Staging flavor entry point
├── main_production.dart ← Production flavor entry point
└── main_common.dart ← Shared code for all flavors
```
**What they do:**
`main_dev.dart` says: "I'm the dev flavor, use dev API!"
```dart
void main() {
final config = FlavorConfig(
flavor: Flavor.dev,
apiBaseUrl: 'https://dev-api.example.com',
);
mainCommon(config); // Run the app with dev config
}
```
### Build Scripts (scripts/)
```
scripts/
├── build-dev.sh ← Build dev flavor (Mac/Linux)
├── build-staging.sh ← Build staging flavor
├── build-production.sh ← Build production flavor
├── build-dev.bat ← Build dev flavor (Windows)
├── build-staging.bat ← Build staging flavor (Windows)
└── build-production.bat ← Build production flavor (Windows)
```
**How to use:**
```bash
# On Mac/Linux
sh scripts/build-production.sh
# On Windows
scripts\build-production.bat
```
## Common Mistakes (And How to Avoid Them)
### ❌ Mistake 1: Forgetting the `-t` flag
```bash
# Wrong - doesn't specify entry point
flutter run --flavor dev
# Right - specifies entry point
flutter run --flavor dev -t lib/main_dev.dart
```
**What happens:** Flutter uses default `lib/main.dart`, which doesn't have flavor config.
### ❌ Mistake 2: Mismatched flavor and entry point
```bash
# Wrong - dev flavor with staging entry point
flutter run --flavor dev -t lib/main_staging.dart
```
**What happens:** App name says "Staging" but uses dev bundle ID. Confusing! 😵
**Right way:**
```bash
# Flavor and entry point match
flutter run --flavor dev -t lib/main_dev.dart
```
### ❌ Mistake 3: Editing generated files
**Don't edit:** `android/app/build.gradle` manually (after setup)
**Why:** Flavor config is auto-generated. Manual edits can break builds.
**If you need changes:** Update the skill inputs and regenerate.
## How to Check Which Flavor is Running
Add this debug code to your app:
```dart
import 'main_common.dart';
// In your widget
Text('Current Flavor: ${FlavorConfig.instance.flavor.name}')
```
**You'll see:**
- "Current Flavor: dev" in dev build
- "Current Flavor: staging" in staging build
- "Current Flavor: production" in production build
## Real-World Example: Building for Testing
**Scenario:** You want to test your app on a physical device with test API.
**Steps:**
1. **Build dev flavor APK:**
```bash
flutter build apk --flavor dev -t lib/main_dev.dart --debug
```
2. **Find the APK:**
```
build/app/outputs/flutter-apk/app-dev-debug.apk
```
3. **Install on device:**
- Copy APK to phone
- Install it
- App name will be "MyApp Dev" 📱
4. **Test with real API:**
```bash
flutter build apk --flavor production -t lib/main_production.dart --release
```
5. **Both can be installed together!**
- MyApp Dev (test version)
- MyApp (real version)
## Key Takeaways for Juniors
✅ **Flavors are like multiple copies of your app** - Each has different settings
✅ **Use dev flavor while developing** - Safe to test without affecting production data
✅ **Use production flavor for release** - Clean, final version for users
✅ **Always match flavor and entry point** - `--flavor dev` goes with `-t lib/main_dev.dart`
✅ **Use build scripts for consistency** - Less typing, fewer mistakes
# For Intermediates: Technical Implementation
## Architecture Overview
### Flavor System Components
```
┌─────────────────────────────────────────────┐
│ Flutter Flavor Architecture │
├─────────────────────────────────────────────┤
│ │
│ Android Side Flutter Side │
│ ───────────── ───────────── │
│ build.gradle main_dev.dart │
│ ↓ ↓ │
│ productFlavors FlavorConfig │
│ ↓ ↓ │
│ Build Variants mainCommon() │
│ ↓ ↓ │
│ APK MyApp() │
│ │
│ iOS Side │
│ ───────── │
│ Xcode Project │
│ ↓ │
│ Build Configurations │
│ ↓ │
│ Schemes │
│ ↓ │
│ IPA │
└─────────────────────────────────────────────┘
```
## Android Implementation Deep Dive
### Gradle Configuration Explained
**File:** `android/app/build.gradle`
```gradle
android {
// Flavor dimension - groups related flavors
flavorDimensions "environment"
productFlavors {
dev {
// Dimension for grouping
dimension "environment"
// Bundle ID suffix (com.example.app → com.example.app.dev)
applicationIdSuffix ".dev"
// App name from string resource
resValue "string", "app_name", "MyApp Dev"
// Environment variables accessible in Dart
buildConfigField "String", "API_BASE_URL", "\"https://dev-api.example.com\""
buildConfigField "boolean", "ENABLE_ANALYTICS", "false"
buildConfigField "boolean", "ENABLE_LOGGING", "true"
}
}
}
```
**What each line does:**
1. **`flavorDimensions "environment"`**
- Groups flavors by category
- You could have multiple dimensions: `["environment", "api_version"]`
- Example: `devV1`, `devV2`, `prodV1`, `prodV2`
2. **`applicationIdSuffix ".dev"`**
- Base ID: `com.example.app` (from `defaultConfig`)
- With suffix: `com.example.app.dev`
- **Why:** Allows dev and prod apps to coexist on same device
3. **`resValue "string", "app_name", "MyApp Dev"`**
- Creates Android string resource dynamically
- Referenced in `AndroidManifest.xml` as `@string/app_name`
- Changes app name without modifying manifest
4. **`buildConfigField`**
- Generates Java constants in `BuildConfig.java`
- **NOT directly accessible in Dart** (Android-only)
- Used if you have native Android code that needs these values
### Build Variants Generated
Gradle automatically creates build variants by combining flavors × build types:
| Flavor | Build Type | Variant Name | Output |
|--------|------------|--------------|--------|
| dev | debug | devDebug | app-dev-debug.apk |
| dev | release | devRelease | app-dev-release.apk |
| staging | debug | stagingDebug | app-staging-debug.apk |
| staging | release | stagingRelease | app-staging-release.apk |
| production | debug | productionDebug | app-production-debug.apk |
| production | release | productionRelease | app-production-release.apk |
**Total:** 3 flavors × 2 build types = **6 variants**
### AndroidManifest.xml Integration
**Before flavors:**
```xml
<application
android:label="MyApp"
android:icon="@mipmap/ic_launcher">
```
**After flavors:**
```xml
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher">
```
**Why the change:**
- `@string/app_name` is defined per flavor in `build.gradle`
- Each flavor gets its own app name automatically
### Flavor-Specific Resources
**Directory structure for flavor-specific icons:**
```
android/app/src/
├── main/
│ └── res/
│ └── mipmap-*/ic_launcher.png (default icons)
├── dev/
│ └── res/
│ └── mipmap-*/ic_launcher.png (dev icons - green)
├── staging/
│ └── res/
│ └── mipmap-*/ic_launcher.png (staging icons - orange)
└── production/
└── res/
└── mipmap-*/ic_launcher.png (production icons - blue)
```
**How it works:**
- Android merges resources from `main/` and `{flavor}/`
- Flavor-specific resources **override** main resources
- If `dev/res/mipmap-hdpi/ic_launcher.png` exists, it overrides `main/res/mipmap-hdpi/ic_launcher.png`
## iOS Implementation Deep Dive
### Xcode Build Configurations
**Standard Xcode configs:**
- Debug
- Release
**After flavors:**
- Debug-dev
- Release-dev
- Debug-staging
- Release-staging
- Debug-production
- Release-production
**How they're created:**
1. Duplicate `Debug` config
2. Rename to `Debug-dev`, `Debug-staging`, `Debug-production`
3. Repeat for `Release`
### Xcode Schemes
**What is a scheme?**
- A scheme defines how to build, run, test, profile, analyze, and archive your app
- Each flavor needs its own scheme
**Scheme configuration:**
| Scheme | Build Configuration (Debug) | Build Configuration (Release) |
|--------|---------------------------|----------------------------|
| dev | Debug-dev | Release-dev |
| staging | Debug-staging | Release-staging |
| production | Debug-production | Release-production |
**Why separate schemes:**
- Allows Xcode to build different flavors
- Each scheme uses different bundle ID and display name
- Can select scheme in Xcode: Product → Scheme → dev
### Info.plist Configuration
**Before flavors:**
```xml
<key>CFBundleDisplayName</key>
<string>MyApp</string>
<key>CFBundleIdentifier</key>
<string>com.example.app</string>
```
**After flavors:**
```xml
<key>CFBundleDisplayName</key>
<string>$(DISPLAY_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
```
**What changed:**
- `$(DISPLAY_NAME)` reads from build settings
- `$(PRODUCT_BUNDLE_IDENTIFIER)` reads from build settings
- Each build configuration sets different values
### Build Settings per Configuration
**Build Settings → User-Defined:**
| Configuration | PRODUCT_BUNDLE_IDENTIFIER | DISPLAY_NAME |
|---------------|--------------------------|--------------|
| Debug-dev | com.example.app.dev | MyApp Dev |
| Release-dev | com.example.app.dev | MyApp Dev |
| Debug-staging | com.example.app.staging | MyApp Staging |
| Release-staging | com.example.app.staging | MyApp Staging |
| Debug-production | com.example.app | MyApp |
| Release-production | com.example.app | MyApp |
**How to set:**
1. Click on Runner project
2. Select Runner target
3. Build Settings tab
4. Click + → Add User-Defined Setting
5. Name: `PRODUCT_BUNDLE_IDENTIFIER`
6. Expand and set values for each configuration
## Flutter-Side Implementation
### Entry Point Pattern
**Traditional single entry point:**
```dart
// lib/main.dart
void main() {
runApp(MyApp());
}
```
**Multi-flavor entry points:**
```dart
// lib/main_dev.dart
void main() {
final config = FlavorConfig(/*dev config*/);
mainCommon(config);
}
// lib/main_staging.dart
void main() {
final config = FlavorConfig(/*staging config*/);
mainCommon(config);
}
// lib/main_production.dart
void main() {
final config = FlavorConfig(/*production config*/);
mainCommon(config);
}
// lib/main_common.dart
void mainCommon(FlavorConfig config) {
FlavorConfig.initialize(config);
runApp(MyApp(config: config));
}
```
**Why this pattern:**
- ✅ **Single source of truth** - All shared logic in `main_common.dart`
- ✅ **Type-safe** - Config is Dart object, not strings
- ✅ **Testable** - Can mock `FlavorConfig` in tests
- ✅ **IDE-friendly** - Auto-complete works
### FlavorConfig Implementation
```dart
class FlavorConfig {
final Flavor flavor;
final String apiBaseUrl;
final bool enableAnalytics;
final bool enableLogging;
final String appName;
const FlavorConfig({
required this.flavor,
required this.apiBaseUrl,
required this.enableAnalytics,
required this.enableLogging,
required this.appName,
});
// Singleton pattern for global access
static FlavorConfig? _instance;
static FlavorConfig get instance {
assert(_instance != null, 'FlavorConfig not initialized');
return _instance!;
}
static void initialize(FlavorConfig config) {
_instance = config;
}
// Convenience methods
bool get isDev => flavor == Flavor.dev;
bool get isStaging => flavor == Flavor.staging;
bool get isProduction => flavor == Flavor.production;
}
```
**Design decisions:**
1. **Why const constructor?**
- Compile-time constant = faster
- Flutter can optimize better
2. **Why singleton pattern?**
- Global access without passing config everywhere
- Alternative: Dependency injection with get_it
3. **Why assert in getter?**
- Fail-fast during development
- Catch initialization errors early
### Using FlavorConfig in Code
**In HTTP client:**
```dart
class ApiClient {
final Dio _dio;
ApiClient() : _dio = Dio() {
_dio.options.baseUrl = FlavorConfig.instance.apiBaseUrl;
if (FlavorConfig.instance.enableLogging) {
_dio.interceptors.add(LogInterceptor());
}
}
}
```
**In analytics:**
```dart
class AnalyticsService {
void logEvent(String name, Map<String, dynamic> params) {
if (!FlavorConfig.instance.enableAnalytics) {
return; // Don't track in dev
}
// Send to analytics
FirebaseAnalytics.instance.logEvent(
name: name,
parameters: params,
);
}
}
```
**In feature flags:**
```dart
class FeatureFlags {
static bool get showBetaFeatures {
return FlavorConfig.instance.isDev ||
FlavorConfig.instance.isStaging;
}
}
```
## Build Process Explained
### Android Build Flow
```
flutter build apk --flavor dev -t lib/main_dev.dart
↓
1. Flutter compiles lib/main_dev.dart → Dart bytecode
↓
2. Flutter generates Android resources
↓
3. Gradle reads build.gradle
↓
4. Gradle selects 'dev' productFlavor
↓
5. Gradle applies applicationIdSuffix → com.example.app.dev
↓
6. Gradle merges resources (main/ + dev/)
↓
7. Gradle compiles Java/Kotlin code
↓
8. Gradle packages → app-dev-release.apk
```
### iOS Build Flow
```
flutter build ios --flavor dev -t lib/main_dev.dart
↓
1. Flutter compiles lib/main_dev.dart → Dart bytecode
↓
2. Flutter generates iOS framework
↓
3. xcodebuild reads Xcode project
↓
4. xcodebuild selects 'dev' scheme
↓
5. xcodebuild reads Debug-dev configuration
↓
6. xcodebuild applies PRODUCT_BUNDLE_IDENTIFIER (com.example.app.dev)
↓
7. xcodebuild applies DISPLAY_NAME (MyApp Dev)
↓
8. xcodebuild compiles Swift/Objective-C code
↓
9. xcodebuild packages → Runner.app
```
## Testing Strategies
### Unit Testing with Flavors
```dart
// test/helpers/test_config.dart
FlavorConfig createTestConfig({
Flavor flavor = Flavor.dev,
String? apiBaseUrl,
bool? enableAnalytics,
}) {
return FlavorConfig(
flavor: flavor,
apiBaseUrl: apiBaseUrl ?? 'https://test-api.example.com',
enableAnalytics: enableAnalytics ?? false,
enableLogging: true,
appName: 'Test App',
);
}
// test/services/api_client_test.dart
void main() {
setUp(() {
FlavorConfig.initialize(createTestConfig());
});
test('uses correct API URL from config', () {
final client = ApiClient();
expect(client.baseUrl, 'https://test-api.example.com');
});
}
```
### Integration Testing with Flavors
```dart
// integration_test/app_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('dev flavor shows debug banner', (tester) async {
final config = FlavorConfig(
flavor: Flavor.dev,
apiBaseUrl: 'https://dev-api.example.com',
enableAnalytics: false,
enableLogging: true,
appName: 'MyApp Dev',
);
await tester.pumpWidget(MyApp(config: config));
// Dev flavor shows debug banner
expect(find.byType(CheckedModeBanner), findsOneWidget);
});
testWidgets('production flavor hides debug banner', (tester) async {
final config = FlavorConfig(
flavor: Flavor.production,
apiBaseUrl: 'https://api.example.com',
enableAnalytics: true,
enableLogging: false,
appName: 'MyApp',
);
await tester.pumpWidget(MyApp(config: config));
// Production flavor hides debug banner
expect(find.byType(CheckedModeBanner), findsNothing);
});
}
```
## VS Code Integration
**`.vscode/launch.json` explained:**
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Dev Flavor", // Name shown in Run dropdown
"request": "launch", // Start the app
"type": "dart", // Dart/Flutter debugger
"program": "lib/main_dev.dart", // Entry point
"args": [
"--flavor", // Pass flavor argument
"dev" // Flavor name
]
}
]
}
```
**How to use:**
1. Open Run and Debug panel (Ctrl+Shift+D)
2. Select "Dev Flavor" from dropdown
3. Click green play button ▶️
4. VS Code runs: `flutter run --flavor dev -t lib/main_dev.dart`
## Debugging Tips
### Issue: "Error: No flavor named 'dev' found"
**Cause:** Android `build.gradle` doesn't have `dev` flavor defined
**Fix:**
1. Open `android/app/build.gradle`
2. Check `productFlavors` block exists
3. Check `dev { }` is defined
4. Run `flutter clean`
5. Run `flutter pub get`
### Issue: "Error: No such file: 'lib/main_dev.dart'"
**Cause:** Entry point file doesn't exist
**Fix:**
```bash
# Create entry point
cat > lib/main_dev.dart << 'EOF'
import 'main_common.dart';
void main() {
final config = FlavorConfig(
flavor: Flavor.dev,
apiBaseUrl: 'https://dev-api.example.com',
enableAnalytics: false,
enableLogging: true,
appName: 'MyApp Dev',
);
mainCommon(config);
}
EOF
```
### Issue: iOS build fails with "No such module"
**Cause:** Xcode scheme not properly configured
**Fix:**
1. Open `ios/Runner.xcworkspace` in Xcode
2. Product → Scheme → Manage Schemes
3. Check that `dev`, `staging`, `production` schemes exist
4. Edit each scheme → Build Configuration
5. Ensure correct configuration selected (Debug-dev for dev scheme)
## Key Takeaways for Intermediates
✅ **Gradle productFlavors** generate build variants (flavor × buildType)
✅ **Xcode schemes** map to build configurations
✅ **FlavorConfig singleton** provides global access to flavor settings
✅ **Entry point pattern** (main_*.dart) enables type-safe configuration
✅ **Flavor-specific resources** override main resources
✅ **VS Code launch.json** simplifies running different flavors
# For Seniors: Advanced Patterns & Best Practices
## Enterprise Architecture Patterns
### 1. Multi-Dimensional Flavors
**Use Case:** Multiple API versions + multiple environments
```gradle
android {
flavorDimensions "environment", "api"
productFlavors {
dev { dimension "environment" }
staging { dimension "environment" }
prod { dimension "environment" }
v1 { dimension "api" }
v2 { dimension "api" }
}
}
```
**Generates 6 flavors:**
- devV1, devV2
- stagingV1, stagingV2
- prodV1, prodV2
**When to use:**
- API versioning during migration
- A/B testing different backends
- Legacy vs modern API support
### 2. Flavor-Based Feature Flags
```dart
class FeatureFlags {
final FlavorConfig _config;
FeatureFlags(this._config);
// Feature available in all non-production environments
bool get enableExperimentalFeatures {
return !_config.isProduction;
}
// Feature available only in dev
bool get enableMockData {
return _config.isDev;
}
// Feature available in staging and production
bool get enablePushNotifications {
return _config.isStaging || _config.isProduction;
}
// Feature available only in production
bool get enableCrashReporting {
return _config.isProduction;
}
// Remote config override
Future<bool> getFeature(String key) async {
// Check flavor-based default
final defaultValue = _getFlavorDefault(key);
// Override with remote config in production
if (_config.isProduction) {
return await RemoteConfig.getBool(key, defaultValue);
}
return defaultValue;
}
}
```
**Pattern Benefits:**
- Progressive rollout (dev → staging → prod)
- Safe testing of features
- Easy rollback (just disable in flavor)
### 3. Environment-Specific Dependency Injection
```dart
// lib/core/di/injection.dart
@module
abstract class AppModule {
@singleton
ApiClient provideApiClient(FlavorConfig config) {
if (config.isDev) {
// Mock API client for offline development
return MockApiClient();
} else if (config.isStaging) {
// Real API with verbose logging
return ApiClient(
baseUrl: config.apiBaseUrl,
interceptors: [LoggingInterceptor(verbose: true)],
);
} else {
// Production API with minimal logging
return ApiClient(
baseUrl: config.apiBaseUrl,
interceptors: [LoggingInterceptor(verbose: false)],
);
}
}
@singleton
AnalyticsService provideAnalytics(FlavorConfig config) {
if (config.isProduction) {
return FirebaseAnalytics();
} else {
return ConsoleAnalytics(); // Log to console in dev/staging
}
}
@singleton
CacheService provideCache(FlavorConfig config) {
if (config.isDev) {
// Aggressive caching in dev (faster builds)
return CacheService(ttl: Duration(hours: 24));
} else {
// Short caching in production (fresh data)
return CacheService(ttl: Duration(minutes: 5));
}
}
}
```
**Pattern Benefits:**
- Environment-specific implementations
- Easy mocking in dev
- Production-grade services in prod
### 4. Flavor-Aware Error Handling
```dart
class ErrorHandler {
final FlavorConfig _config;
ErrorHandler(this._config);
void handleError(Object error, StackTrace stackTrace) {
// Always log to console in dev
if (_config.isDev || _config.isStaging) {
debugPrint('Error: $error');
debugPrint('Stack trace: $stackTrace');
}
// Report to crash reporting in staging + production
if (!_config.isDev) {
_reportToCrashlytics(error, stackTrace);
}
// Show detailed error dialog in dev
if (_config.isDev) {
_showDetailedErrorDialog(error, stackTrace);
} else {
// Show user-friendly message in production
_showGenericErrorDialog();
}
}
void _reportToCrashlytics(Object error, StackTrace stackTrace) {
FirebaseCrashlytics.instance.recordError(
error,
stackTrace,
reason: 'Caught by ErrorHandler',
information: [
'flavor: ${_config.flavor.name}',
'apiUrl: ${_config.apiBaseUrl}',
],
);
}
}
```
## CI/CD Integration Patterns
### 1. GitHub Actions Multi-Flavor Build
```yaml
name: Multi-Flavor Build
on:
push:
branches: [main, develop]
pull_request:
jobs:
build-android:
runs-on: ubuntu-latest
strategy:
matrix:
flavor: [dev, staging, production]
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
- name: Install dependencies
run: flutter pub get
- name: Build APK
run: |
flutter build apk \
--flavor ${{ matrix.flavor }} \
-t lib/main_${{ matrix.flavor }}.dart \
--release
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-${{ matrix.flavor }}-release
path: build/app/outputs/flutter-apk/app-${{ matrix.flavor }}-release.apk
build-ios:
runs-on: macos-latest
strategy:
matrix:
flavor: [dev, staging, production]
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
- name: Install dependencies
run: flutter pub get
- name: Build iOS
run: |
flutter build ios \
--flavor ${{ matrix.flavor }} \
-t lib/main_${{ matrix.flavor }}.dart \
--release \
--no-codesign
- name: Archive IPA
run: |
cd build/ios/iphoneos
mkdir Payload
cp -R Runner.app Payload/
zip -r app-${{ matrix.flavor }}-release.ipa Payload
- name: Upload IPA
uses: actions/upload-artifact@v3
with:
name: app-${{ matrix.flavor }}-ios
path: build/ios/iphoneos/app-${{ matrix.flavor }}-release.ipa
```
**What this does:**
- Builds all 3 flavors in parallel
- Uploads artifacts for download
- Runs on every push to main/develop
### 2. Automated Distribution
```yaml
# .github/workflows/distribute-flavors.yml
name: Distribute Flavors
on:
push:
tags:
- 'v*'
jobs:
distribute-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- name: Build dev APK
run: flutter build apk --flavor dev -t lib/main_dev.dart --release
- name: Upload to Firebase App Distribution (Dev)
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID_DEV }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIALS }}
groups: internal-testers
file: build/app/outputs/flutter-apk/app-dev-release.apk
distribute-staging:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- name: Build staging APK
run: flutter build apk --flavor staging -t lib/main_staging.dart --release
- name: Upload to Firebase App Distribution (Staging)
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID_STAGING }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIALS }}
groups: qa-testers, stakeholders
file: build/app/outputs/flutter-apk/app-staging-release.apk
publish-production:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- name: Build production app bundle
run: flutter build appbundle --flavor production -t lib/main_production.dart --release
- name: Upload to Play Store (Internal Testing)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_CREDENTIALS }}
packageName: com.example.app
releaseFiles: build/app/outputs/bundle/productionRelease/app-production-release.aab
track: internal
```
**Distribution Strategy:**
- **Dev** → Firebase App Distribution (internal testers)
- **Staging** → Firebase App Distribution (QA + stakeholders)
- **Production** → Google Play (internal testing track)
### 3. Automated Testing per Flavor
```yaml
# .github/workflows/test-flavors.yml
name: Test Flavors
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- name: Run unit tests
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
integration-tests:
runs-on: ubuntu-latest
strategy:
matrix:
flavor: [dev, staging, production]
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- name: Run integration tests (${{ matrix.flavor }})
run: |
flutter test integration_test \
--flavor ${{ matrix.flavor }} \
-t lib/main_${{ matrix.flavor }}.dart
```
## Advanced Security Patterns
### 1. Flavor-Specific API Keys
**Problem:** Don't hardcode API keys in code
**Solution:** Environment variables + secrets
**Android:**
```gradle
// android/app/build.gradle
android {
productFlavors {
dev {
// Read from environment variable
def apiKey = System.getenv("DEV_API_KEY") ?: "dev-placeholder-key"
buildConfigField "String", "API_KEY", "\"${apiKey}\""
}
production {
def apiKey = System.getenv("PROD_API_KEY") ?: "prod-placeholder-key"
buildConfigField "String", "API_KEY", "\"${apiKey}\""
}
}
}
```
**Flutter:**
```dart
// lib/core/config/secrets.dart
class Secrets {
static const String apiKey = String.fromEnvironment(
'API_KEY',
defaultValue: 'placeholder-key',
);
}
// Usage
final apiClient = ApiClient(apiKey: Secrets.apiKey);
```
**Build with secrets:**
```bash
# Dev
export DEV_API_KEY="sk-dev-12345"
flutter build apk --flavor dev -t lib/main_dev.dart --dart-define=API_KEY=$DEV_API_KEY
# Production
export PROD_API_KEY="sk-prod-67890"
flutter build apk --flavor production -t lib/main_production.dart --dart-define=API_KEY=$PROD_API_KEY
```
**CI/CD with secrets:**
```yaml
# .github/workflows/build.yml
- name: Build production
env:
API_KEY: ${{ secrets.PROD_API_KEY }}
run: |
flutter build apk \
--flavor production \
-t lib/main_production.dart \
--dart-define=API_KEY=$API_KEY \
--release
```
### 2. Certificate Pinning per Flavor
```dart
class ApiClient {
final FlavorConfig _config;
late final Dio _dio;
ApiClient(this._config) {
_dio = Dio();
// Setup certificate pinning for production only
if (_config.isProduction) {
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.badCertificateCallback =
(X509Certificate cert, String host, int port) {
// Pin production certificate SHA-256
const productionCertSHA256 = 'ABCD1234...';
final certSHA256 = sha256.convert(cert.der).toString();
return certSHA256 == productionCertSHA256;
};
return client;
};
}
}
}
```
**Why flavor-specific:**
- Dev/staging may use self-signed certificates
- Production requires strict certificate validation
- Prevents MITM attacks in production
## Performance Optimization Patterns
### 1. Flavor-Specific Build Optimization
```gradle
// android/app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
}
}
productFlavors {
dev {
// Faster builds in dev (no optimization)
minifyEnabled false
shrinkResources false
}
production {
// Full optimization in production
minifyEnabled true
shrinkResources true
}
}
}
```
**Build time comparison:**
- Dev: ~30 seconds (no minification)
- Production: ~2 minutes (full optimization)
### 2. Conditional Plugin Loading
```dart
// lib/core/plugins/plugins.dart
class Plugins {
static Future<void> initialize(FlavorConfig config) async {
// Always initialize core plugins
await Firebase.initializeApp();
// Analytics only in staging + production
if (!config.isDev) {
await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(true);
}
// Crashlytics only in production
if (config.isProduction) {
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
}
// Mock services in dev
if (config.isDev) {
await MockService.initialize();
}
}
}
```
**Benefits:**
- Faster app launch in dev (fewer plugins)
- Reduced memory usage
- No accidental test data in analytics
## Monorepo Strategies
### 1. Shared Flavors Across Multiple Apps
**Directory structure:**
```
my_company/
├── shared/
│ └── flavors/
│ ├── flavor_config.dart
│ └── main_common.dart
├── app1/
│ ├── lib/
│ │ ├── main_dev.dart
│ │ ├── main_staging.dart
│ │ └── main_production.dart
│ └── pubspec.yaml
└── app2/
├── lib/
│ ├── main_dev.dart
│ ├── main_staging.dart
│ └── main_production.dart
└── pubspec.yaml
```
**Shared flavor config:**
```yaml
# shared/flavors/pubspec.yaml
name: company_flavors
version: 1.0.0
dependencies:
flutter:
sdk: flutter
```
**App pubspec:**
```yaml
# app1/pubspec.yaml
dependencies:
company_flavors:
path: ../shared/flavors
```
**Benefits:**
- Consistent flavor logic across all apps
- Update once, applies everywhere
- Centralized environment configuration
### 2. Flavor Inheritance Pattern
```dart
// shared/flavors/base_flavor_config.dart
abstract class BaseFlavorConfig {
Flavor get flavor;
String get apiBaseUrl;
bool get enableAnalytics;
bool get enableLogging;
// Common methods all apps need
bool get isDev => flavor == Flavor.dev;
bool get isStaging => flavor == Flavor.staging;
bool get isProduction => flavor == Flavor.production;
Map<String, dynamic> toJson();
}
// app1/lib/config/app1_flavor_config.dart
class App1FlavorConfig extends BaseFlavorConfig {
final String app1SpecificKey;
@override
final Flavor flavor;
@override
final String apiBaseUrl;
// ... implement other base properties
const App1FlavorConfig({
required this.flavor,
required this.apiBaseUrl,
required this.app1SpecificKey,
// ...
});
}
```
## Debugging & Monitoring
### 1. Flavor-Aware Logging
```dart
// lib/core/logging/logger.dart
class AppLogger {
final FlavorConfig _config;
final Logger _logger;
AppLogger(this._config)
: _logger = Logger(
level: _config.isDev ? Level.verbose : Level.warning,
printer: _config.isDev
? PrettyPrinter()
: SimplePrinter(), // Less verbose in production
);
void debug(String message, [dynamic error, StackTrace? stackTrace]) {
if (_config.isDev || _config.isStaging) {
_logger.d(message, error, stackTrace);
}
}
void info(String message) {
_logger.i('[${_config.flavor.name}] $message');
}
void warning(String message) {
_logger.w('[${_config.flavor.name}] $message');
// Send to monitoring service in production
if (_config.isProduction) {
_sendToMonitoringService(message, 'WARNING');
}
}
void error(String message, [dynamic error, StackTrace? stackTrace]) {
_logger.e('[${_config.flavor.name}] $message', error, stackTrace);
// Always send errors to monitoring
if (!_config.isDev) {
_sendToMonitoringService(message, 'ERROR', error, stackTrace);
}
}
}
```
### 2. Visual Flavor Indicator
```dart
// lib/core/widgets/flavor_banner.dart
class FlavorBanner extends StatelessWidget {
final Widget child;
final FlavorConfig config;
const FlavorBanner({
Key? key,
required this.child,
required this.config,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Show banner only in non-production flavors
if (config.isProduction) {
return child;
}
return Banner(
message: config.flavor.name.toUpperCase(),
location: BannerLocation.topEnd,
color: _getBannerColor(),
child: child,
);
}
Color _getBannerColor() {
switch (config.flavor) {
case Flavor.dev:
return Colors.green;
case Flavor.staging:
return Colors.orange;
case Flavor.production:
return Colors.transparent;
}
}
}
// Usage in main_common.dart
runApp(
FlavorBanner(
config: config,
child: MyApp(config: config),
),
);
```
## Migration Strategies
### 1. Migrating Existing App to Flavors
**Step-by-step migration:**
**Phase 1: Create flavor structure (no breaking changes)**
1. Create `lib/main_common.dart` from existing `lib/main.dart`
2. Create `lib/main_production.dart` that calls `mainCommon()`
3. Update build commands to use `-t lib/main_production.dart`
4. **No functional changes yet** - app works exactly the same
**Phase 2: Add dev flavor**
1. Create `lib/main_dev.dart`
2. Update `android/app/build.gradle` with dev flavor
3. Configure iOS dev scheme
4. Test: `flutter run --flavor dev -t lib/main_dev.dart`
**Phase 3: Add staging flavor**
1. Create `lib/main_staging.dart`
2. Update `android/app/build.gradle` with staging flavor
3. Configure iOS staging scheme
4. Test: `flutter run --flavor staging -t lib/main_staging.dart`
**Phase 4: Move configuration to FlavorConfig**
1. Extract hardcoded values (API URLs, keys) to FlavorConfig
2. Update code to use `FlavorConfig.instance`
3. Test all 3 flavors thoroughly
### 2. Zero-Downtime Flavor Rollout
**Strategy for live apps:**
1. **Keep existing build working**
- Don't remove `lib/main.dart` immediately
- Add flavors alongside existing setup
2. **Gradual transition**
- Release production flavor first (identical to current)
- Internal testing with dev/staging flavors
- Once stable, deprecate old build process
3. **Rollback plan**
- Keep old build scripts for 1-2 releases
- Document rollback procedure
- Have old build process in separate branch
## Best Practices Summary
### DO ✅
1. **Use consistent naming** - dev, staging, production (not test, prod, live)
2. **Match flavor and entry point** - `--flavor dev` with `-t lib/main_dev.dart`
3. **Version control flavor configs** - Commit all flavor files
4. **Document flavor differences** - Update README with flavor details
5. **Test all flavors in CI** - Don't just test default flavor
6. **Use environment variables for secrets** - Never hardcode API keys
7. **Visual indicators in non-prod** - Banners, different colors
8. **Automate builds** - Scripts for each flavor
### DON'T ❌
1. **Don't hardcode environment values** - Use FlavorConfig
2. **Don't mix flavor logic with business logic** - Separate concerns
3. **Don't forget iOS configuration** - Android is easier, iOS needs manual setup
4. **Don't test only in dev** - Production may have unique issues
5. **Don't use flavors for feature flags** - Use remote config instead
6. **Don't create too many flavors** - 3-4 is ideal, more is chaos
7. **Don't modify generated files** - Update source and regenerate
8. **Don't skip documentation** - Future you will thank present you
## Performance Metrics
**Build time comparison (average):**
| Flavor | Debug Build | Release Build | App Size |
|--------|------------|---------------|----------|
| Dev | 30s | 2m | 25 MB |
| Staging | 35s | 2.5m | 22 MB |
| Production | 40s | 3m | 18 MB |
**Why differences:**
- Dev: No minification, includes debug symbols
- Production: Full optimization, shrunk resources
## Key Takeaways for Seniors
✅ **Multi-dimensional flavors** for complex scenarios (environment × API version)
✅ **Flavor-based feature flags** for progressive rollout
✅ **Environment-specific DI** for optimal implementations
✅ **CI/CD matrix builds** for parallel flavor builds
✅ **Flavor-aware monitoring** for environment-specific logging
✅ **Migration strategies** for zero-downtime transitions
✅ **Monorepo patterns** for shared flavor logic
✅ **Security patterns** for flavor-specific API keys and certificate pinning
# Quick Reference
## Common Commands
```bash
# Run flavors
flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor staging -t lib/main_staging.dart
flutter run --flavor production -t lib/main_production.dart
# Build debug APK
flutter build apk --flavor dev -t lib/main_dev.dart --debug
# Build release APK
flutter build apk --flavor production -t lib/main_production.dart --release
# Build app bundle (Play Store)
flutter build appbundle --flavor production -t lib/main_production.dart --release
# Build iOS
flutter build ios --flavor production -t lib/main_production.dart --release
# Clean build
flutter clean && flutter pub get
```
## File Locations
| File | Purpose |
|------|---------|
| `android/app/build.gradle` | Android flavor configuration |
| `ios/Runner.xcodeproj` | iOS flavor configuration (Xcode) |
| `lib/main_dev.dart` | Dev flavor entry point |
| `lib/main_staging.dart` | Staging flavor entry point |
| `lib/main_production.dart` | Production flavor entry point |
| `lib/main_common.dart` | Shared flavor logic |
| `lib/core/config/environment.dart` | Environment configuration class |
| `.vscode/launch.json` | VS Code debug configurations |
| `scripts/build-*.sh` | Build scripts (Mac/Linux) |
| `scripts/build-*.bat` | Build scripts (Windows) |
## Troubleshooting Quick Fixes
| Issue | Solution |
|-------|----------|
| "No flavor named 'dev'" | Add flavor to `build.gradle`, run `flutter clean` |
| "No such file: main_dev.dart" | Create entry point file |
| iOS build fails | Configure Xcode schemes manually |
| Wrong API URL | Check FlavorConfig in entry point |
| Can't install dev + prod together | Verify different bundle IDs |
## Additional Resources
- **Flutter Flavors Documentation:** https://flutter.dev/docs/deployment/flavors
- **Android Product Flavors:** https://developer.android.com/studio/build/build-variants
- **Xcode Schemes:** https://developer.apple.com/documentation/xcode/customizing-the-build-schemes-for-a-project
**Version:** 1.0.0
**Last Updated:** 2025-10-24
**Skill ID:** flutter-flavors
**Category:** development-workflow