# 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

```bash
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](https://docs.flutter.dev/deployment/android#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`](https://doc.saymyframe.com/modules/dart-define.md) also passes `--dart-define-from-file=.env` to each of these commands; see [Values in .env](https://doc.saymyframe.com/guides/env.md). 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:

```dart
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](https://doc.saymyframe.com/guides/env.md), 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](https://doc.saymyframe.com/guides/firebase.md#one-project-for-each-flavor) | 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](https://doc.saymyframe.com/setup.md) 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](https://doc.saymyframe.com/guides/ci.md#values-for-each-environment) | 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](https://doc.saymyframe.com/guides/app-icon-and-splash.md).

## 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:

```kotlin
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](https://doc.saymyframe.com/guides/firebase.md#one-project-for-each-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:

```bash
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](https://doc.saymyframe.com/extending/role-provider.md#the-app-entry) describes the rest.
