← Articles

Setting up Flutter flavors: dev, staging, and production

By Ann Tech · 8 September 2024

Flutter flavors let you run the same codebase as three completely separate apps — dev, staging, and production — each with its own app ID, Firebase project, API URL, and app icon.

What flavors solve

Without flavors:

  • Dev and production share the same Firebase project (logs pollute analytics)
  • You can't install dev and production simultaneously on a device
  • Testers might accidentally use the dev version in production

With flavors, each environment is a distinct app to both the OS and the user.

Setting up flavors on Android

android/app/build.gradle.kts:

android {
  flavorDimensions += "environment"

  productFlavors {
    create("dev") {
      dimension = "environment"
      applicationId = "com.example.myapp.dev"
      versionNameSuffix = "-dev"
      resValue("string", "app_name", "MyApp Dev")
    }
    create("staging") {
      dimension = "environment"
      applicationId = "com.example.myapp.staging"
      versionNameSuffix = "-staging"
      resValue("string", "app_name", "MyApp Staging")
    }
    create("production") {
      dimension = "environment"
      applicationId = "com.example.myapp"
      resValue("string", "app_name", "MyApp")
    }
  }
}

Setting up flavors on iOS

  1. In Xcode → project → Configurations, add Debug-dev, Debug-staging, Release-staging, Release-production (duplicate existing).
  2. Create Schemes: dev, staging, production. Each scheme points to the corresponding configuration.
  3. Set bundle IDs per scheme in Build Settings: PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp.dev.

Or use flutter_flavorizr to automate all of this:

# pubspec.yaml
flavorizr:
  flavors:
    dev:
      app:
        name: "MyApp Dev"
      android:
        applicationId: "com.example.myapp.dev"
      ios:
        bundleId: "com.example.myapp.dev"
    staging:
      app:
        name: "MyApp Staging"
      android:
        applicationId: "com.example.myapp.staging"
      ios:
        bundleId: "com.example.myapp.staging"
    production:
      app:
        name: "MyApp"
      android:
        applicationId: "com.example.myapp"
      ios:
        bundleId: "com.example.myapp"
flutter pub run flutter_flavorizr

Entry points per flavor

// lib/main_dev.dart
void main() {
  AppConfig.init(environment: Env.dev, apiBaseUrl: 'http://localhost:3000');
  runApp(const MyApp());
}

// lib/main_staging.dart
void main() {
  AppConfig.init(environment: Env.staging, apiBaseUrl: 'https://api.staging.example.com');
  runApp(const MyApp());
}

// lib/main.dart (production)
void main() {
  AppConfig.init(environment: Env.production, apiBaseUrl: 'https://api.example.com');
  runApp(const MyApp());
}

Running a specific flavor

# Dev
flutter run --flavor dev -t lib/main_dev.dart

# Staging
flutter run --flavor staging -t lib/main_staging.dart

# Production build
flutter build appbundle --flavor production -t lib/main.dart
flutter build ipa --flavor production -t lib/main.dart

Firebase per flavor

Each flavor needs its own google-services.json / GoogleService-Info.plist:

android/app/src/dev/google-services.json
android/app/src/staging/google-services.json
android/app/src/production/google-services.json

ios/Runner/Firebase/dev/GoogleService-Info.plist
ios/Runner/Firebase/staging/GoogleService-Info.plist
ios/Runner/Firebase/production/GoogleService-Info.plist

For iOS, a pre-build script in Xcode copies the right file:

# Xcode build phase (Run Script)
cp "${SRCROOT}/Runner/Firebase/${FLAVOR}/GoogleService-Info.plist" \
   "${SRCROOT}/Runner/GoogleService-Info.plist"

App icons per flavor

Place flavor-specific icons in:

android/app/src/dev/res/mipmap-hdpi/ic_launcher.png
android/app/src/staging/res/mipmap-hdpi/ic_launcher.png

For iOS, use Xcode asset catalogs with AppIcon-dev, AppIcon-staging, AppIcon and set the correct one per scheme.

flutter_flavorizr handles icon setup if you provide source images in your pubspec.yaml flavorizr config.

Fastlane + flavors

# Fastfile
lane :deploy_staging do
  build_flutter(
    flavor: 'staging',
    target: 'lib/main_staging.dart',
    build_mode: 'release',
  )
  upload_to_testflight
end

Common pitfalls

Forgetting -t lib/main_flavor.dart. Running flutter run --flavor dev without specifying the target will run lib/main.dart with the dev flavor — which might initialize the production config. Always pair --flavor with -t.

All flavors sharing one Firebase project. Dev events in Firebase Analytics pollute production metrics. Always use separate Firebase projects per environment.

Not having a staging environment. Going directly from dev to production means the first time production config is tested is in front of real users. staging with production-equivalent config catches environment-specific issues before release.

Sign in to like, dislike, or report.

Setting up Flutter flavors: dev, staging, and production — ANN Tech