easy_flavor 0.1.1 copy "easy_flavor: ^0.1.1" to clipboard
easy_flavor: ^0.1.1 copied to clipboard

A CLI that sets up Flutter flavors (dev, staging, prod, ...) for Android and iOS in new or existing projects, with optional Firebase integration.

easy_flavor #

Set up Flutter flavors (dev, staging, prod, ...) for Android and iOS in one command, in a new or existing project, with optional Firebase per flavor.

easy_flavor init --flavors dev,staging,prod
flutter run --flavor dev -t lib/main_dev.dart
  • Android productFlavors (Groovy or Kotlin DSL), app names, manifest label
  • Xcode build configurations, schemes, xcconfig files, Podfile mapping
  • Dart entry points and a FlavorConfig class
  • Optional Firebase per flavor through the FlutterFire CLI
  • VS Code and Android Studio run configurations
  • Safe for existing projects: dry runs with diffs, git checks, backups, restore, idempotent re-runs, and merging with flavors you already have

Contents #

Install #

dart pub global activate easy_flavor

Make sure ~/.pub-cache/bin is on your PATH. Requirements:

  • Dart 3.8 or newer (bundled with Flutter 3.32+)
  • Flutter 3.22 or newer (for default-flavor support)
  • For iOS: macOS with Xcode
  • For Firebase: the Firebase CLI and the FlutterFire CLI

Quick start #

New project:

easy_flavor create my_app --org com.company --flavors dev,staging,prod
cd my_app
flutter run --flavor dev -t lib/main_dev.dart

Existing project (run from the project root):

easy_flavor init --flavors dev,prod --dry-run --verbose   # preview with diffs
easy_flavor init --flavors dev,prod

Plain flutter run (no --flavor/-t) keeps working and starts the default flavor (the production one: prod, production, release or live, otherwise the first).

Commands #

Options available on every command:

Option Description
--dry-run Print the planned changes (and commands) without writing anything.
-v, --verbose Detailed output; with --dry-run, a unified diff of every file.
--[no-]interactive Prompt for missing values. Off automatically when not in a terminal.
-C, --project-dir <path> Project root (for create: the parent directory).
--version Print the version.

create #

Runs flutter create, then sets up flavors.

easy_flavor create my_app --org com.company \
  --flavors dev,staging,prod \
  --bundle-ids dev=com.company.myapp.dev,staging=com.company.myapp.stg,prod=com.company.myapp \
  --app-names dev="MyApp Dev",staging="MyApp Stg",prod="MyApp" \
  --firebase --firebase-projects dev=myapp-dev,staging=myapp-stg,prod=myapp-prod
Option Description
--org Reverse-DNS organization (default com.example).
--description pubspec description.
--platforms android, ios (default both).
-f, --flavors Flavor names, e.g. dev,staging,prod. Defaults to dev,staging,prod (asked interactively).
--bundle-ids flavor=id pairs used for Android and iOS.
--android-ids, --ios-bundle-ids Per-platform ids (override --bundle-ids).
--app-names flavor=name pairs. Quote names with spaces.
--[no-]firebase Configure Firebase per flavor.
--firebase-projects flavor=project-id pairs.
--ide vscode, android-studio (default both).

Default ids are <org>.<app>.<flavor>; the production flavor gets <org>.<app>. Default names are <App Name> <Flavor>, and the production flavor gets <App Name>.

init #

Adds flavors to an existing project. Takes the same flavor options as create, plus:

Option Description
--[no-]merge The project already has flavors: adopt them instead of stopping (asked interactively).
--force Continue even if git has uncommitted changes.
easy_flavor init --flavors dev,prod --firebase

The existing Android applicationId and iOS bundle identifier become the production ids; other flavors get a .<flavor> suffix. Running init again re-applies easy_flavor.yaml and changes nothing if the project is up to date, so it is also how you apply edits you make to the YAML file.

add / remove #

easy_flavor add qa --app-name "MyApp QA" --bundle-id com.company.myapp.qa
easy_flavor remove qa

add options: --app-name, --bundle-id, --android-id, --ios-bundle-id, --firebase-project, --force. Without them, values are derived from the production flavor. remove options: -y/--yes (no confirmation), --force.

remove deletes everything easy_flavor generated for the flavor (Gradle and Xcode entries, xcconfig files, scheme, entry point, run configurations). Files you added (icons, google-services.json, GoogleService-Info.plist) are kept.

doctor #

easy_flavor doctor

Checks that Gradle flavors, Xcode configurations, schemes, xcconfig files, the Podfile map, Info.plist, Dart entry points and Firebase files all match easy_flavor.yaml. It also warns when the iOS deployment target is below what the installed Xcode accepts. Exits non-zero if it finds errors.

restore #

Every run that changes an existing project first backs up the files it touches to .easy_flavor_backup/<timestamp>/ (and adds that folder to .gitignore).

easy_flavor restore --list        # list backups
easy_flavor restore               # undo the latest run
easy_flavor restore 20261018-153012

easy_flavor.yaml #

All settings live in easy_flavor.yaml at the project root, so the setup is reproducible. Commands read it; flags override it; the file is updated after each run.

flavors:
  dev:
    app_name: "MyApp Dev"
    android_id: com.company.myapp.dev
    ios_bundle_id: com.company.myapp.dev
    firebase_project: myapp-dev      # only used with firebase: true
  prod:
    app_name: "MyApp"
    android_id: com.company.myapp
    ios_bundle_id: com.company.myapp
    firebase_project: myapp-prod
firebase: true
ide: [vscode, android-studio]
merge_existing: true                 # written by `init --merge`

Per-flavor fields are optional; missing ones get the defaults described above. Flavor names must be lowerCamelCase identifiers (dev, staging, qa2) and cannot be Gradle/Dart reserved words (main, test, debug, release, profile, class, ...).

What gets generated #

Area Files
Dart lib/flavors.dart, lib/main_<flavor>.dart; lib/main.dart refactored (see below); flutter: default-flavor: in pubspec.yaml
Android A marked productFlavors block in android/app/build.gradle(.kts) with applicationId and resValue app_name; android:label="@string/app_name" in AndroidManifest.xml; android/app/src/<flavor>/
iOS Debug-, Release-, Profile-<flavor> configurations in ios/Runner.xcodeproj; ios/Flutter/<Config>-<flavor>.xcconfig with PRODUCT_BUNDLE_IDENTIFIER, APP_DISPLAY_NAME, EASY_FLAVOR; a shared scheme per flavor; CFBundleDisplayName = $(APP_DISPLAY_NAME) in Info.plist; the ios/Podfile configuration map (if you use CocoaPods)
Firebase lib/firebase/firebase_options_<flavor>.dart, android/app/src/<flavor>/google-services.json, ios/config/<flavor>/GoogleService-Info.plist, and an Xcode build phase that copies the right plist
IDE .vscode/launch.json entries, .run/<flavor>.run.xml (Android Studio)

easy_flavor edits files precisely rather than overwriting them:

  • Gradle: only the block between // easy_flavor:begin and // easy_flavor:end is managed. Both Groovy and Kotlin DSL are supported.

  • Xcode: project.pbxproj is read and written with a real parser that reproduces Xcode's formatting, so diffs only show actual changes. New object ids are deterministic.

  • main.dart is never deleted. Its main() is renamed to runMainApp(), and a new main() delegates to the default flavor's entry point:

    import 'main_prod.dart' as default_flavor; // easy_flavor:default-flavor
    
    // Plain `flutter run` (without -t) starts the default flavor.
    Future<void> main() => default_flavor.main();
    
    void runMainApp() {
      runApp(const MyApp());
    }
    

Using flavors in Dart #

Each entry point calls FlavorConfig.init(Flavor.<name>) before runMainApp():

import 'flavors.dart';

final config = FlavorConfig.instance;
Text(config.appTitle);
if (config.flavor == Flavor.dev) showDebugBanner();
final api = Uri.parse(config.baseUrl);

lib/flavors.dart is regenerated when flavors change, but your edits to the baseUrl: lines are kept (one line per flavor, a constant expression). Put other per-flavor settings in your own code with a switch on FlavorConfig.instance.flavor.

Firebase #

  1. Install and log in:

    npm install -g firebase-tools       # or: curl -sL https://firebase.tools | bash
    dart pub global activate flutterfire_cli
    firebase login
    

    On macOS, FlutterFire also needs the Ruby xcodeproj gem (gem install xcodeproj; CocoaPods users already have it).

  2. Create one Firebase project per flavor (or reuse one for several).

  3. Run:

    easy_flavor init --firebase --firebase-projects dev=myapp-dev,prod=myapp-prod
    

    Without --firebase-projects, you are asked for each flavor's project id (or set firebase_project in easy_flavor.yaml).

For each flavor, easy_flavor runs:

flutterfire configure --yes --project=<id> \
  --out=lib/firebase/firebase_options_<flavor>.dart \
  --android-package-name=<android_id> --android-out=android/app/src/<flavor>/google-services.json \
  --ios-bundle-id=<ios_bundle_id> --ios-out=ios/config/<flavor>/GoogleService-Info.plist \
  --ios-build-config=Release-<flavor>

It also adds firebase_core to pubspec.yaml, and makes each main_<flavor>.dart call Firebase.initializeApp with that flavor's options. On iOS, a build phase named [easy_flavor] Copy GoogleService-Info.plist copies ios/config/$(EASY_FLAVOR)/GoogleService-Info.plist into the app, so builds don't need the FlutterFire CLI.

FlutterFire changes the Xcode project itself (through Ruby). easy_flavor undoes those edits and uses its own build phase, so the project file stays under easy_flavor's control. FlutterFire's Gradle changes (the google-services plugin) are kept.

Projects that already used Firebase: an existing ios/Runner/GoogleService-Info.plist is removed from the Runner's Copy Bundle Resources (the file stays in the project), because the build phase above now provides the plist. Also remove the old Firebase.initializeApp call from runMainApp(), since each entry point initialises Firebase with its flavor's options, and delete the old lib/firebase_options.dart and android/app/google-services.json.

Re-running init only calls flutterfire configure again for flavors whose project or ids changed, or whose files are missing. Delete a flavor's options file to force it.

Existing projects #

Before changing an existing project, easy_flavor:

  • Checks git. Uncommitted changes stop the run unless you confirm (or pass --force), so its changes are easy to review. Outside git you get a warning.
  • Backs up every file it will touch to .easy_flavor_backup/<timestamp>/. Use easy_flavor restore to undo.
  • Shows the plan first. Use --dry-run --verbose to see a diff without writing anything.

Projects that already have flavors #

If your Gradle file or Xcode project already declares flavors, init stops and offers to merge (--merge on CI):

easy_flavor init --merge --dry-run --verbose
easy_flavor init --merge

When merging, easy_flavor:

  • Imports your flavors' names, ids and app names into easy_flavor.yaml. An id shared by several flavors is not imported, so each flavor gets its own id; check the table before confirming.
  • Leaves your Gradle productFlavors block alone. Its own block goes after yours, in your flavor dimension, and sets applicationId (clearing any applicationIdSuffix) and app_name. buildConfigField, signing and other settings in your block keep working.
  • Leaves your Xcode configurations' xcconfig files alone and sets PRODUCT_BUNDLE_IDENTIFIER, APP_DISPLAY_NAME and EASY_FLAVOR on them.
  • Keeps your own lib/main_<flavor>.dart files and existing schemes.

CI #

Prompts are disabled automatically when there is no terminal (or with --no-interactive), so pass everything as flags or in easy_flavor.yaml:

easy_flavor init --flavors dev,prod --force
easy_flavor doctor

For Firebase on CI, set FIREBASE_TOKEN (from firebase login:ci) or GOOGLE_APPLICATION_CREDENTIALS (service account JSON). easy_flavor skips the login check and passes them on to FlutterFire.

Troubleshooting #

iOS build fails: "deployment target ... is set to 13.0, but the range of supported deployment target versions is 15.0 to ..." Xcode 27 and newer need iOS 15.0+, but older Flutter templates use 13.0. easy_flavor doctor reports this. Set IPHONEOS_DEPLOYMENT_TARGET to 15.0 for the Runner project and target (all configurations), and in the Podfile use platform :ios, '15.0'.

Android: "Product Flavor ... contains custom resource values, but the feature is disabled" Newer Android Gradle Plugin versions disable resValue. The generated block enables it (buildFeatures { resValues = true }). Re-run easy_flavor init if you removed it.

Android: "Duplicate resources" for app_name This means app_name is defined in main/res/values/strings.xml. easy_flavor detects that and writes android/app/src/<flavor>/res/values/strings.xml instead of using resValue. Re-run easy_flavor init if you added the string later.

"Runner.xcscheme not found" Flavor schemes are copies of the shared Runner scheme. In Xcode, open Product > Scheme > Manage Schemes and tick Shared for Runner (or copy ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme from a fresh flutter create).

CocoaPods: "did not set the base configuration of your project" A warning about the plain Profile configuration using Flutter/Release.xcconfig comes from the Flutter template and is unrelated to flavors. The flavor configurations include their own Pods xcconfig.

flutterfire fails with "cannot load such file -- xcodeproj" Install the gem: gem install xcodeproj.

"Stopped because of uncommitted changes" Commit or stash, or pass --force.

Something went wrong. Run easy_flavor restore to undo the last run, then easy_flavor doctor. Please open an issue with the --dry-run --verbose output.

Limitations #

  • Android and iOS only (no macOS, web, Windows or Linux flavors).
  • The iOS app target is the Flutter Runner target (or, without one, the first application target).
  • Merging works with a single Gradle flavor dimension.
  • A flavor declared in your own Gradle productFlavors (merge mode) must be deleted there before easy_flavor remove can remove it.
  • --app-names splits on commas, so app names containing commas must be set in easy_flavor.yaml. In iOS display names, $(...) is expanded by Xcode.
  • FlutterFire writes GoogleService-Info.plist only on macOS; run Firebase setup on a Mac for iOS.
  • FlutterFire's Crashlytics debug-symbol upload script is not kept, because easy_flavor restores the Xcode project after FlutterFire runs. Add it manually if you use Crashlytics.

License #

MIT

0
likes
160
points
--
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A CLI that sets up Flutter flavors (dev, staging, prod, ...) for Android and iOS in new or existing projects, with optional Firebase integration.

Repository (GitHub)
View/report issues

Topics

#flutter #flavors #cli #firebase #build-variants

License

MIT (license)

Dependencies

args, mason_logger, meta, path, yaml, yaml_edit

More

Packages that depend on easy_flavor