CI/CD & App Store Deployment

Build Flavors and Product Flavors in Flutter

16 min Lesson 3 of 12

Build Flavors and Product Flavors in Flutter

Most production apps need more than one build variant. A build flavor (called a product flavor on Android or a scheme/target on iOS) lets you compile the same codebase into multiple distinct apps — for example dev, staging, and production — each with its own bundle ID, app name, icon, API endpoint, and signing identity. Flutter's --flavor flag bridges both platforms so a single command drives the correct native configuration.

Note: Flavors are a native concept: Android handles them in Gradle, iOS handles them in Xcode. Flutter does not invent a new mechanism — it maps the --flavor argument directly to the matching Gradle product flavor or Xcode scheme name. You must configure both platforms independently.

Why Use Flavors?

  • Parallel installation: com.example.app.dev and com.example.app are different bundle IDs, so they can coexist on the same device.
  • Safe testing: Testers hit a staging API; end-users hit production — with no code change between builds.
  • Distinct branding: Development builds can show a red icon or a "DEV" suffix in the app name so you never accidentally ship a debug build.
  • CI/CD integration: Your pipeline passes --flavor production and the correct artifact is produced automatically.

Configuring Flavors on Android (Gradle)

Open android/app/build.gradle and add a flavorDimensions line plus a productFlavors block inside the android {} closure:

android/app/build.gradle — product flavors

android {
    // ...existing config...

    flavorDimensions "environment"

    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            resValue "string", "app_name", "MyApp DEV"
        }
        staging {
            dimension "environment"
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            resValue "string", "app_name", "MyApp Staging"
        }
        production {
            dimension "environment"
            // No suffix — this is the release bundle ID
            resValue "string", "app_name", "MyApp"
        }
    }
}

Each flavor can override the application ID, version name, resource values (like the app label), and even point to flavor-specific source sets (e.g. src/dev/ for a dev-only google-services.json).

Tip: Place your flavor-specific google-services.json files under android/app/src/dev/, android/app/src/staging/, and android/app/src/production/. Gradle automatically picks the right file when building the corresponding flavor.

Configuring Flavors on iOS (Xcode)

iOS uses schemes and build configurations rather than Gradle flavors. The recommended approach is:

  • Duplicate the default Runner target for each environment (or use a single target with multiple build configurations).
  • Create one Xcode scheme per flavor — name it exactly the same as the Gradle flavor (dev, staging, production). Flutter matches by name.
  • In each scheme's Build Configuration, set a unique PRODUCT_BUNDLE_IDENTIFIER and PRODUCT_NAME.

ios/Runner/Info.plist — use a build-setting variable for the name

<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>

<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

Then in each Xcode build configuration (Dev, Staging, Production), define APP_DISPLAY_NAME and PRODUCT_BUNDLE_IDENTIFIER with their environment-specific values. This keeps Info.plist generic while each configuration injects the right values at build time.

Reading the Flavor at Runtime with Dart

Flutter can pass the flavor name into your Dart code via --dart-define. Combine it with the --flavor flag so you always know the current environment:

lib/config/environment.dart — flavor-aware configuration

// Pass at build time:
// flutter run --flavor dev --dart-define=FLAVOR=dev
// flutter build apk --flavor production --dart-define=FLAVOR=production

enum AppFlavor { dev, staging, production }

class Environment {
  static const String _flavor =
      String.fromEnvironment('FLAVOR', defaultValue: 'dev');

  static AppFlavor get flavor {
    switch (_flavor) {
      case 'staging':
        return AppFlavor.staging;
      case 'production':
        return AppFlavor.production;
      default:
        return AppFlavor.dev;
    }
  }

  static String get apiBaseUrl {
    switch (flavor) {
      case AppFlavor.dev:
        return 'https://api-dev.example.com';
      case AppFlavor.staging:
        return 'https://api-staging.example.com';
      case AppFlavor.production:
        return 'https://api.example.com';
    }
  }

  static bool get isProduction => flavor == AppFlavor.production;
  static bool get showDebugBanner => !isProduction;
}

Flavor-Specific App Icons

Use the flutter_launcher_icons package with multiple configuration files — one per flavor. Name each config file after its flavor:

  • flutter_launcher_icons-dev.yaml
  • flutter_launcher_icons-staging.yaml
  • flutter_launcher_icons-production.yaml

Run dart run flutter_launcher_icons -f flutter_launcher_icons-dev for each flavor. The package places the generated icons into the correct flavor source set automatically.

Warning: The Xcode scheme name must match the --flavor string exactly (case-sensitive). If your Gradle flavor is dev but your Xcode scheme is Dev (capital D), the iOS build will fail. Establish a consistent lowercase naming convention across both platforms from the start.

Running and Building with Flavors

Once both platforms are configured, use the --flavor flag with every Flutter command:

Common flavor commands

# Run the dev flavor on a connected device
flutter run --flavor dev --dart-define=FLAVOR=dev

# Build a staging APK
flutter build apk --flavor staging --dart-define=FLAVOR=staging

# Build a production App Bundle for the Play Store
flutter build appbundle --flavor production --dart-define=FLAVOR=production --release

# Build for iOS (Xcode scheme must exist)
flutter build ios --flavor production --dart-define=FLAVOR=production --release

Summary

Build flavors are the professional standard for shipping multiple environment variants from one codebase. Android defines them as productFlavors in Gradle; iOS defines them as schemes with distinct build configurations. Flutter's --flavor flag wires both together. Combine flavors with --dart-define to inject environment-specific values into Dart, and use per-flavor icon configs to give each build a visually distinct launcher icon. This setup eliminates manual file swapping, prevents production accidents, and integrates cleanly with any CI/CD pipeline.