UNPKG

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
# 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