easy_flavor 0.1.1
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
FlavorConfigclass - 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
- Quick start
- Commands
- easy_flavor.yaml
- What gets generated
- Using flavors in Dart
- Firebase
- Existing projects
- CI
- Troubleshooting
- Limitations
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-flavorsupport) - 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:beginand// easy_flavor:endis managed. Both Groovy and Kotlin DSL are supported. -
Xcode:
project.pbxprojis 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 torunMainApp(), and a newmain()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 #
-
Install and log in:
npm install -g firebase-tools # or: curl -sL https://firebase.tools | bash dart pub global activate flutterfire_cli firebase loginOn macOS, FlutterFire also needs the Ruby
xcodeprojgem (gem install xcodeproj; CocoaPods users already have it). -
Create one Firebase project per flavor (or reuse one for several).
-
Run:
easy_flavor init --firebase --firebase-projects dev=myapp-dev,prod=myapp-prodWithout
--firebase-projects, you are asked for each flavor's project id (or setfirebase_projectineasy_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>/. Useeasy_flavor restoreto undo. - Shows the plan first. Use
--dry-run --verboseto 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
productFlavorsblock alone. Its own block goes after yours, in your flavor dimension, and setsapplicationId(clearing anyapplicationIdSuffix) andapp_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_NAMEandEASY_FLAVORon them. - Keeps your own
lib/main_<flavor>.dartfiles 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
Runnertarget (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 beforeeasy_flavor removecan remove it. --app-namessplits on commas, so app names containing commas must be set ineasy_flavor.yaml. In iOS display names,$(...)is expanded by Xcode.- FlutterFire writes
GoogleService-Info.plistonly 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