Skip to content

Guides

Flutter flavors: dev, staging and production for Android and iOS ​

Short answer. A flavor is a named build of your app with its own settings: app name, app ID, icon and API address. On Android a flavor is a Gradle product flavor. On iOS it is an Xcode scheme with its own build configurations. Flutter joins them with one flag, --flavor. With flavors you can install the dev, staging and production builds side by side on one phone.

This guide shows the setup in a plain Flutter project. The last section shows how Archipelago generates the same setup.

What a flavor changes ​

SettingWhy you want it to differ
App ID (applicationId, bundle ID)Dev and production install side by side, and each has its own store listing and push token
App name and iconYou can tell the builds apart on the home screen
API address and keysDev talks to a test server, production talks to the real one
Logging, debug toolsOn in dev, off in production

Step 1: Android product flavors ​

Open android/app/build.gradle.kts and add flavorDimensions and productFlavors inside the android block. This follows the Flutter flavors guide.

kotlin
android {
    // ...
    flavorDimensions += "default"
    productFlavors {
        create("development") {
            dimension = "default"
            applicationIdSuffix = ".development"
            resValue(type = "string", name = "app_name", value = "MyApp Dev")
        }
        create("staging") {
            dimension = "default"
            applicationIdSuffix = ".staging"
            resValue(type = "string", name = "app_name", value = "MyApp Staging")
        }
        create("production") {
            dimension = "default"
            resValue(type = "string", name = "app_name", value = "MyApp")
        }
    }
}

In android/app/src/main/AndroidManifest.xml, point the label at the new resource:

xml
<application
    android:label="@string/app_name"
    ... />

Production has no suffix, so it keeps your real app ID. If your project still uses build.gradle (Groovy), the same blocks work with Groovy syntax.

For a different icon per flavor, put icons in android/app/src/<flavor>/res/mipmap-*/ic_launcher.png. Gradle uses the flavor folder before main.

Step 2: iOS schemes and build configurations ​

Xcode has no "flavor". You make a scheme and give it its own configurations. Flutter looks for a scheme with the same name as the flavor. These steps follow the Flutter iOS flavors guide.

  1. Open ios/Runner.xcworkspace in Xcode.
  2. Choose Product > Scheme > New Scheme, pick the Runner target, and name the scheme staging. Repeat for development and production. The schemes must be shared (check the Shared box in Manage Schemes).
  3. Select the Runner project, open the Info tab, and under Configurations duplicate Debug, Profile and Release once per flavor. Name them Debug-staging, Profile-staging, Release-staging and so on. Use lowercase flavor names so the Flutter tool recognizes them.
  4. In Manage Schemes, edit each scheme so Run and Test use Debug-<flavor>, Profile uses Profile-<flavor>, and Archive uses Release-<flavor>.
  5. In the Runner target, open Build Settings and set a different Product Bundle Identifier for each configuration, for example com.example.myapp.staging.
  6. If your project already has a Podfile, map the new configurations to a build mode:
ruby
project 'Runner', {
  'Debug' => :debug,
  'Debug-development' => :debug,
  'Debug-staging' => :debug,
  'Debug-production' => :debug,
  'Profile' => :release,
  'Profile-development' => :release,
  'Profile-staging' => :release,
  'Profile-production' => :release,
  'Release' => :release,
  'Release-development' => :release,
  'Release-staging' => :release,
  'Release-production' => :release,
}

To set a different app name, add a user-defined build setting such as APP_DISPLAY_NAME with one value per configuration, and set CFBundleDisplayName to $(APP_DISPLAY_NAME) in Info.plist.

Register each bundle ID in your Apple Developer account before you sign a build.

Step 3: Tell the Dart code which flavor it is ​

You have two options, and many apps use both.

Read the flavor. Flutter exposes the flavor you passed to --flavor as appFlavor. It is null when you build without a flavor.

dart
import 'package:flutter/services.dart';

void main() {
  final apiUrl = switch (appFlavor) {
    'production' => 'https://api.example.com',
    'staging' => 'https://staging.api.example.com',
    _ => 'https://dev.api.example.com',
  };
  runApp(MyApp(apiUrl: apiUrl));
}

Use one entry point per flavor. Each file sets up its own config and calls the same app.

dart
// lib/main_staging.dart
import 'package:flutter/material.dart';
import 'app.dart';

void main() => runApp(const MyApp(apiUrl: 'https://staging.api.example.com'));

Entry points give you a place for flavor-only code, such as a debug banner in dev. A single main.dart with appFlavor is shorter. Pick one and keep it.

Step 4: Keep per-flavor values in a file ​

Do not write every value in Dart. Flutter can read a JSON file at build time with --dart-define-from-file:

json
{
  "API_URL": "https://staging.api.example.com",
  "SENTRY_ENABLED": "true"
}
dart
const apiUrl = String.fromEnvironment('API_URL');
const sentryEnabled = bool.fromEnvironment('SENTRY_ENABLED');

Keep one file per flavor, for example config/staging.json, and pass the one you need to each build command.

A value compiled into an app can be extracted from the app. The Flutter docs call storing secrets in an app a poor security practice. Use this for addresses and feature switches, and keep real secrets on your server.

Step 5: Run and build ​

Pass the flavor and, if you use entry points, the target:

bash
flutter run --flavor staging -t lib/main_staging.dart --dart-define-from-file=config/staging.json

flutter build apk --flavor production --dart-define-from-file=config/production.json
flutter build appbundle --flavor production --dart-define-from-file=config/production.json
flutter build ipa --flavor production --dart-define-from-file=config/production.json

In VS Code, add one entry per flavor to .vscode/launch.json with "args": ["--flavor", "staging"]. In Android Studio, add the flavor under Additional run args in the run configuration.

Common problems ​

  • "The Xcode project does not define custom schemes" or a missing scheme error. The scheme name must match the --flavor value, and the scheme must be shared.
  • Configuration names not found. Use the pattern Debug-<flavor>, Profile-<flavor> and Release-<flavor>, with a lowercase flavor.
  • Android builds but the app name does not change. The manifest label is still a plain string. Change it to @string/app_name.
  • Firebase. On Android, put google-services.json in android/app/src/<flavor>/. On iOS, copy the right GoogleService-Info.plist per configuration with a Run Script build phase.
  • Two flavors overwrite each other on the phone. They share one app ID. Add an applicationIdSuffix on Android and a different bundle ID on iOS.

Automate the native setup with flutter_flavorizr ​

The Xcode steps are slow by hand and easy to get wrong. The flutter_flavorizr package writes the Gradle, scheme and Podfile changes from one file. Add it as a dev dependency, then create flavorizr.yaml:

yaml
flavors:
  development:
    app:
      name: "MyApp Dev"
    android:
      applicationId: "com.example.myapp.development"
    ios:
      bundleId: "com.example.myapp.development"
  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"
bash
dart run flutter_flavorizr

Review the diff after you run it. The tool edits native files, so commit before you start.

How Archipelago does this ​

archipelago create generates an app with development, staging and production flavors already set up. You do not run flavorizr yourself.

  • The app has lib/main_development.dart, lib/main_staging.dart and lib/main.dart (production). Each one passes a FlavorStatus value to the app's bootstrap function.
  • The devtools/ toolkit has a flavor command. flavor add --name qa --generate adds a flavor, runs flutter_flavorizr, and creates the missing entry file.
  • Flavors are defined once in flavors.yaml and synced to every app in the workspace.
  • The env command keeps one .env.<flavor> file per flavor and copies the one you need into .env before a build. The values go through envied, which obfuscates them. Only the active flavor is compiled into the binary.
  • The generated Fastlane lanes take flavor: and pass --flavor and the right entry file to flutter build. See Flutter CI/CD with Fastlane and GitHub Actions.

References: Flavor Manager, Environment Sync, and the monorepo_toolkit brick.

FAQ ​

Do I need flavors if I only have one backend? ​

If you ship to the stores and also test builds, yes. Two builds with the same app ID replace each other on the phone. Flavors give each build its own app ID.

What is the difference between a flavor and a build type? ​

A build type is how the app is compiled: debug, profile or release. A flavor is which version of the app you build: development, staging or production. You get every combination, for example stagingRelease.

Can I use --dart-define instead of flavors? ​

For values such as an API address, yes. It cannot change the app ID, name, icon or native files, so you still need flavors for builds that must install side by side.

Why does my iOS flavor fail with a missing scheme? ​

Flutter looks for an Xcode scheme with the same name as --flavor. Create it, share it, and check the spelling and case.

Does this work for macOS? ​

Yes. The macOS setup follows the iOS one, with Xcode schemes and configurations. See the Flutter docs for the exact differences.

Try Archipelago

Make a free account, install the CLI, and generate a project:

dart pub global activate archipelago_cli
archipelago auth login --interactive
archipelago create --output ./my_app

The Free plan includes 3 project scaffolds per month and the Free bricks. See Getting Started for each step, and pricing for Pro and Enterprise.