Flavors
Every app that smf create generates has three native flavors, dev, staging and prod. You get them without an option or a module. They are plain Android and Xcode settings, so the app has no package for them. Each flavor has an id of its own, so all three install next to each other on one device:
| Flavor | Android application id | iOS bundle id | Name on the device |
|---|---|---|---|
dev | com.example.my_app.dev | com.example.my-app.dev | My App Dev |
staging | com.example.my_app.staging | com.example.my-app.staging | My App Staging |
prod | com.example.my_app | com.example.my-app | My App |
The ids start with the organization and the name of the app. smf create --explain prints the ids of prod, and the section "Flavors" of the README of the app lists all of them.
Run and build a flavor
flutter run # dev
flutter run --flavor staging
flutter build apk --flavor prod
flutter build appbundle --flavor prod
flutter build ipa --flavor prod
A flutter command without --flavor builds dev, because pubspec.yaml says default-flavor: dev. That holds in release mode too: flutter build appbundle is a release of dev, under its id. So a build for a store always names --flavor prod.
On Android, a release build is signed with the debug key until android/app/build.gradle.kts has a signing config of yours, and a store refuses such a build. Sign the app tells how.
The name of the flavor is part of the name of each APK and app bundle, as in build/app/outputs/flutter-apk/app-prod-release.apk and build/app/outputs/bundle/prodRelease/app-prod-release.aab.
An app with the module dart_define also passes --dart-define-from-file=.env to each of these commands; see Values in .env. The values of .env are not tied to a flavor.
Read the flavor in code
The app has one entry, lib/main.dart, for all flavors, so add no main_<flavor>.dart. Code reads the flavor from appFlavor of Flutter:
import 'package:flutter/services.dart';
final showDebugMenu = appFlavor != 'prod';
appFlavor is a constant of the build. flutter test runs as dev, the default flavor.
Run configurations
An app with a provider of the values in .env, such as dart_define, has a run configuration for each flavor, named after the app and the flavor, such as my_app staging. Each passes the flavor and the arguments that the values need:
| IDE | Where they are | What runs them |
|---|---|---|
| VS Code | .vscode/launch.json | Run and Debug (F5) starts the one that you select in the Run and Debug view. The Run and Debug links above main() and above a test, and the Testing view, always use the one of dev, the only one that is a template of those launches (templateFor). |
| Android Studio | .run/<flavor>.run.xml | Each is a run configuration of its own. A test that you run from Android Studio gets none of these arguments, so run tests from a terminal with them. |
An app without such a provider has no run configurations. There, an IDE runs dev, and you pass --flavor as an argument for another flavor.
Xcode
Each flavor has a shared scheme, ios/Runner.xcodeproj/xcshareddata/xcschemes/<flavor>.xcscheme, and the build configurations Debug-<flavor>, Profile-<flavor> and Release-<flavor>.
In Xcode, select the scheme of a flavor. It builds that flavor, with its id, its name and its appFlavor, whatever the last flutter command did.
The scheme Runner stays in the project, on the plain Debug, Profile and Release configurations, and those have the id and the name of prod. It takes appFlavor from ios/Flutter/Generated.xcconfig, which holds what the last flutter command wrote there and has no flavor after a fresh flutter pub get. In an app with Firebase, a run of the scheme Runner can then stop at start-up, because the app cannot tell which options to use. flutter build ios --config-only --flavor <flavor> gives that file a flavor.
What repeats for each flavor
A service that knows your app by its id, such as a store or a push provider, knows each flavor as an app of its own. The setup of such a service repeats for each flavor that needs it. SMF does not hide this, because the services work that way with or without SMF. A flavor that never reaches a store or a real user may need none of it.
| What | How often |
|---|---|
| Firebase | Each flavor runs dart tool/configure_firebase.dart once. Flavors may share a project or have one each. |
| The steps by hand in a Firebase project | Once for each project, or once for each app of a project, as the guide of each step says. |
| The App ID of the iOS app | Once for each bundle id that you sign and that needs a capability. |
| Products in App Store Connect and Google Play Console | For each id that sells. A store knows com.example.my_app.staging as another app. |
| Values of CI | One GitHub environment for each flavor that you build there. |
The icon and the splash screen are the same for all flavors; see App icon and splash screen.
Add or remove a flavor
After generation, the flavors are text in the files of your app. SMF does not touch them again. These steps add a flavor qa. The name has no hyphen, since Flutter splits the name of a build configuration at it.
1. Android. Add it to productFlavors in android/app/build.gradle.kts, with an id suffix and a name on the device:
create("qa") {
dimension = "default"
applicationIdSuffix = ".qa"
manifestPlaceholders["appName"] = "My App QA"
}
2. iOS. In ios/Runner.xcodeproj/project.pbxproj, each of the three configuration lists gets Debug-qa, Profile-qa and Release-qa, copies of those of another flavor. In Xcode, that is Duplicate under the configurations of the project. In the Runner target, give the three copies the bundle id in PRODUCT_BUNDLE_IDENTIFIER and the name on the device in APP_DISPLAY_NAME. Then copy xcschemes/staging.xcscheme to xcschemes/qa.xcscheme and replace -staging with -qa in it. The schemes of the app already have the pre-action that Flutter needs, so a copy keeps it.
3. Run configurations. If the app has them, add one to .vscode/launch.json, and copy .run/staging.run.xml to .run/qa.run.xml.
4. Firebase. If the app has firebase_core, copy lib/firebase_options_staging.dart to lib/firebase_options_qa.dart, add the flavor to the switch in lib/firebase_options_of_flavor.dart and to _flavors at the top of tool/configure_firebase.dart. Then configure it as for any flavor.
5. CI. If the app has github_actions, add the flavor to options under workflow_dispatch in .github/workflows/ci.yml.
Then check it:
flutter build apk --debug --flavor qa
flutter build ios --config-only --flavor qa
To remove a flavor, undo the same places. default-flavor in pubspec.yaml has to name a flavor that stays, and the plain configurations in the Xcode project stay as they are.
A setting that you add to the Runner target by hand goes into each of its twelve build configurations, the three plain ones and the nine of the flavors.
A module of your own
The flavors are constants of the app entry role, AppEntryRole.flavors, each an AppFlavor, so a module reads their names and ids from there rather than writing them out. What a provider of the app entry keeps to describes the rest.