Flavors in Flutter with Fastlane - yet another guide
Tags: flutter, mobile, fastlane
Published: 2019-07-15 19:48:57

---
title: Flavors in Flutter with Fastlane - yet another guide
published: true
description: It's a good practice to build separate apps for development, test and production environment. In case of mobile apps a good way to have separate configurations is usage of flavors. In this tutorial you will learn how to prepare ordinary Flutter project to have 3 different flavors and how to handle CI/CD with fastlane.
tags: flutter, mobile, fastlane
---

It's a good practice to build separate apps for development, test and production environment. In case of mobile apps a good way to have separate configurations is usage of flavors.

In this tutorial you will learn how to prepare ordinary Flutter project to have 3 different flavors (**dev**, **test** and **production**) and how to handle build, signing and deployment with fastlane.

**TL;DR** Just go to [the repository](https://github.com/orestesgaolin/flutter-flavors) where all the flavors are already configured.

> You can also read the tutorial without large gifs [on my blog](https://roszkowski.dev/2019/flutter-flavors/).

## Basics

The concept of flavors is taken from Android apps and can be applied to iOS in various ways (more on this later). By incorporating flavors in your project you can build your app with different configuration options, styles or feature sets. In commercial projects it's a standard way of distributing apps.

There are several great articles on build flavors just to mention:

* [Creating flavors of a Flutter app (Flutter & Android setup)](https://cogitas.net/creating-flavors-of-a-flutter-app/) by Natalie Masse Hooper,
* [Flavoring Flutter](https://medium.com/@salvatoregiordanoo/flavoring-flutter-392aaa875f36) by Salvatore Giordano,
* [Flutter Ready to Go (flavors, connectivity and more)](https://medium.com/flutter-community/flutter-ready-to-go-e59873f9d7de#c38c) by Julio Henrique Bitencourt. 

In this article I'll show a similar but a subtly different approach and focus mostly on iOS part. Presented way works really well for me and my colleagues. It's been battle tested with several apps already and is getting better with each new project.

For instance our test builds have AppCenter distribution packages to automate updates and additional logging included, dev builds have very verbose logging, and production apps come without unnecessary diagnostics but with production logging configuration.

Flutter comes with built-in flavor support but default project is not prepared to handle them. All it takes to define flavors is to add and edit few files. There are multiple ways to achieve this and with each new project you'll have a chance to improve your approach. Especially on iOS there are multiple ways to provide different bundle ids or configuration parameters.

### Fastlane

In my daily job I use **fastlane** to automate apps deployment to QA and app stores. In this article I will show how to use flavors with fastlane but in general you can handle flavors manually or in typical CI environment like Codemagic or Bitrise.

Fastlane allows you to define specific _lanes_ for each app in code like `deploy_to_appcenter` or `deploy_to_store`. A set of files can describe signing, build and deployment phases. Those can be reproduced both on developer's computer, but also on CI/CD platform. Fastlane allows to automate provisioning and signing of iOS apps as well as screenshot capture or updating the description in store. This gives us a very convenient and reproducible way of distributing our app.

There is no native support of Flutter apps in fastlane but we can define fastlane configuration for Android and iOS projects and treat them as typical native apps.

## Flavors in Dart

### Preparation

In this article I use Flutter v1.7.8+hotfix.3 and demo app is created with Kotlin, AndroidX, and Swift support by:

![flutter create -i swift -a kotlin --androidx --org com.flutter flutter_flavors](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/flutter-create.png)

It's a good practice to create new projects with Kotlin and Swift support. AndroidX is a future of Android development so while starting new project you should definitely have it enabled. You will benefit from Swift later in your project when you'll have to write some platform specific code.

### How to configure Flutter project

In order to take the advantage of flavors in Flutter app you should define 3 separate main files[^1] that will handle all the configuration details different for each scheme. The easiest way is to rename `main.dart` to `main_common.dart` and create:

* `main_dev.dart`
* `main_tst.dart`
* `main_prod.dart`

In each of them you can define respective configuration and later just start execution of the app from a common function.

```dart
import 'dart:async';

import 'package:flutter_flavors/app_config.dart';
import 'package:flutter_flavors/main_common.dart';

Future<void> main() async { // async can be useful if you fetch from disk or network
  // do flavor specific configuration here e.g. API endpoints
  final config = AppConfig('tst');

  mainCommon(config);
}
```

The `AppConfig` class is a used to store some basic configuration options like name or API endpoints.

In `main_common.dart` you should replace the 3rd line with:

```dart
void mainCommon(AppConfig config) => runApp(MyApp(config));
```

This step of the configuration you can investigate in [commit a4c7ef8e](https://github.com/orestesgaolin/flutter-flavors/commit/a4c7ef8e351676825b422839baa539ecf7f021d9).

### How to build or run Flutter project

Typically you would run following commands to build flavored app:

**Ordinary apk:** `flutter build apk --release -t lib/main_tst.dart --build-name=1.0.0 --build-number=1 --flavor tst `

**App bundle:** `flutter build appbundle --target-platform android-arm,android-arm64 --release -t lib/main_tst.dart --build-name=1.0.0 --build-number=1 --flavor tst`

**iOS:** `flutter build ios --release --no-codesign -t lib/main_tst.dart --build-name=1.0.0 --build-number=1 --flavor tst`

Some important things to notice here:

* we define build numbers (`1`) and build names (`1.0.0`)
* we use **tst** flavor
* we skip codesign for iOS (we'll sign our app with fastlane)
* we'll sign our android app later

In order to run the app with desired flavor in VS Code you can define your own `launch.json` configuration. Below you may find a sample I use in my apps. You may copy it to the configuration file that opens when you click the cog wheel on debug pad in VS Code.


![click this cog wheel in VS Code](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/vscode-cog.png)

```json
{
    "version": "0.2.0",
    "configurations": [
      {
        "name": "Flutter Dev",
        "request": "launch",
        "type": "dart",
        "flutterMode": "debug",
        "program": "lib/main_dev.dart",
        "args": [
          "--flavor",
          "dev"
        ]
      },
      {
        "name": "Flutter Dev Release",
        "request": "launch",
        "type": "dart",
        "flutterMode": "release",
        "program": "lib/main_dev.dart",
        "args": [
          "--flavor",
          "dev"
        ]
      },
      {
        "name": "Flutter Profile",
        "request": "launch",
        "type": "dart",
        "flutterMode": "profile",
        "program": "lib/main_dev.dart",
        "args": [
          "--flavor",
          "dev"
        ]
      },
      {
        "name": "Flutter Test",
        "request": "launch",
        "type": "dart",
        "flutterMode": "release",
        "program": "lib/main_tst.dart",
        "args": [
          "--flavor",
          "tst"
        ]
      },
      {
        "name": "Flutter Prod",
        "request": "launch",
        "type": "dart",
        "flutterMode": "release",
        "program": "lib/main_prod.dart",
        "args": [
          "--flavor",
          "prod"
        ]
      },
    ]
  }
  
```

At this point these commands would fail because we haven't defined flavors in Android and iOS apps yet.

## Flavors on Android

Defining flavors on Android is really straightforward. The only file to be changed is `build.gradle` in `app` directory.

Just add the following lines after buildTypes node and before closing bracket:

```groovy
    flavorDimensions "flavor-type"

    productFlavors {
        dev {
            dimension "flavor-type"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            manifestPlaceholders = [appName: "Flavor DEV"]
        }
        tst {
            dimension "flavor-type"
            applicationIdSuffix ".test"
            versionNameSuffix "-test"
            manifestPlaceholders = [appName: "Flavor TST"]
        }
        prod {
            dimension "flavor-type"
            manifestPlaceholders = [appName: "Flavor"]
        }
    }
```

You can take a look at the exact diff here in [commit cef5fbff](https://github.com/orestesgaolin/flutter-flavors/commit/cef5fbffea052c9c26f93aa4ab63d3434b0a44e1).

Some important notes here:

* we have different app ids for each flavor: `com.flutter.flutter_flavor.dev`, `com.flutter.flutter_flavor.test`, and `com.flutter.flutter_flavor` - this way you can install all 3 apps on a single device, have separate google-services.json files and distinct the app in some logging service or Firebase
* we set different app names
* we set different version name suffixes e.g. `1.0.0` becomes `1.0.0-test`

<img align="right" width="300" src="https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/android-res.png">

Flavors on Android allow us to define separate resources for each of them. E.g. you can have a special icon for QA builds or different strings resources.
What you need to do to provide new icon is just create `mipmap-...` folders with icons in `app/src/tst` directory. The same works for any other resources and schemes.

```sh
app
| - src
   | - debug (default)
   | - main (default)
   | - profile (default)
   | - tst (add this with desired subdirectories)
```

At this point you should be able to build 3 separate flavors of the app for Android.

## Flavors on iOS

Typically, in iOS apps you can base flavors on build schemes. In order to configure this you'll need macOS and Xcode. To start you should open `ios/Runner.xcworkspace` in Xcode.

![iOS project open in Xcode](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-xcode.png)

### Creating schemes

Default scheme for Flutter apps is Runner. We'll define additional 3 schemes named exactly as the previously defined flavors i.e. `dev`, `tst` and `prod`. You should go to `Product > Scheme > Manage` schemes and add them via `+` button. Make sure the schemes are marked as _Shared_.

![creating iOS schemes in Xcode](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-xcode-scheme.gif)

See the [mp4](https://roszkowski.dev/assets/images/ios-xcode-scheme.mp4)/[webm](https://roszkowski.dev/assets/images/ios-xcode-scheme.webm)

Then you should add 3 xconfig files to Flutter directory next to Debug, Release and Generated. Right click on Flutter directory on left pad in Xcode and select `New File`. Select `Configuration Settings File` and add `dev.xconfig`, `tst.xconfig` and `prod.xconfig`. Make sure they're in Flutter directory as seen on the screenshot below.

![iOS configuration files](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-xcode-xconfig.png)

These files allow you to define custom variables that can be used later during build or in `Info.plist` file. We'll define our custom app bundle ids here.

My typical `dev.xconfig` files look like follows:

```c
#include "Generated.xcconfig"
BUNDLE_ID_SUFFIX=.dev
PRODUCT_BUNDLE_IDENTIFIER=com.flutter.flutterflavors.dev
FLUTTER_TARGET=lib/main_dev.dart
APP_NAME=Flavor DEV
```

and `tst.xconfig` (note `.test` suffix, not `.tst`[^2]):

```c
#include "Generated.xcconfig"
BUNDLE_ID_SUFFIX=.test
PRODUCT_BUNDLE_IDENTIFIER=com.flutter.flutterflavors.test
FLUTTER_TARGET=lib/main_tst.dart
APP_NAME=Flavor TST
```

### Extending configuration

At this point you should copy and paste some build configurations and assign them to the respective scheme. There will be a lot of clicking and typing now so be patient.

Go to project settings in Xcode, select `Runner` and then `Debug` in _Configurations_ section. Press Enter to rename it to `Debug-dev`. Then duplicate it and call it `Debug-tst`, and another with `Debug-prod`. Repeat the procedure for `Release` and `Profile` configurations. Then assign previously created schemes to respective configurations. You should end up with following layout:

![iOS Xcode configurations](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-xcode-configs.png)

This should allow you to build your app with different bundle id per flavor. To make sure you can go to Build Settings of Runner target and look for `Product Bundle Identifier` position.

![iOS product bundle identifiers](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-xcode-packaging.png)

There is still one problem to be solved. When building the app Flutter takes into account the product bundle identifier visible in _General_ tab of the target properties. So even with `tst` flavor you'll see following output in console:

![flutter build ios --release --no-codesign -t lib/main_tst.dart --build-number=1 --build-name=1.0.0 --flavor tst](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-signing.png)

Take a look at the wrong bundle id for `Release-tst` scheme.

Fortunately, if we defined a `PRODUCT_BUNDLE_IDENTIFIER` variable in our `tst.xconfig` file this will be overwritten during the build so that generating and signing the `.test` bundle id will be possible.

### Archiving

Finally, we should update each build scheme with correct build configuration.

Go to `Product > Schemes > Manage Schemes`, select `dev` and click `Edit`. Now for each of the processes (Run, Test, Profile, Analyze, Archive) change the build configuration to `-dev` one. Repeat the process for `tst` and `prod` schemes.

![iOS build configs updated](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-xcode-build-configs.png)


## Signing iOS app with fastlane

In order to sign and provision your app you'll need Apple developer account and fastlane configured. I recommend creating a separate 'service' account for fastlane only with separate certificate. Create 3 application identifiers in Apple Developer portal e.g. `com.flutter.flutterflavors`, `com.flutter.flutterflavors.test`, and `com.flutter.flutterflavors.dev`.

Go to `ios` folder in your console and initialize fastlane with manual mode (option 4.). In fastlane folder create `Matchfile` file next to `Fastfile` and `Appfile`.

My typical `Matchfile` looks like this:

```ruby
# you should store your provisioning profiles and certs in repository
# this repository is encrypted with MATCH_PASSWORD env variable
git_url(ENV["FASTLANE_GIT"])
storage_mode("git")
username(ENV["FASTLANE_USERNAME"])
team_id(ENV["FASTLANE_TEAM"])
# this is useful on CI/CD if you build test and production app 
# flavors with the same steps configuration
app_identifier(ENV["APP_NAME"])
type("development")
```

After creating application ids and adding the files you should be able to generate provisioning profiles. Execute following commands and type desired bundle id when prompted:

```
bundle exec fastlane match development
bundle exec fastlane match adhoc
bundle exec fastlane match release
```

This whole iOS step can be observed in [commit 162d2015](https://github.com/orestesgaolin/flutter-flavors/commit/162d2015cdab401fb1eafa8e06da88fa71351e16).

### Rebuilding and signing with fastlane

Unfortunately, it is necessary to rebuild iOS app to archive it and sign before deploying to testers or AppStore[^3]. With custom flavors it is necessary to provide provisioning profile match map manually. I couldn't make the fastlane to detect all profiles automatically. If anyone knows better way to do this, then please share!

My typical `Fastfile` for QA/test builds looks as follows[^4]:

```ruby
# update_fastlane

default_platform(:ios)

platform :ios do
  desc "Submit a new build to AppCenter"
  lane :test do
    # add_badge(dark: true)
    register_devices(
        devices_file: "fastlane/devices.txt",
        team_id: ENV["FASTLANE_TEAM"],
        username: ENV["FASTLANE_USERNAME"]
    )
    match(
      type: "adhoc",
      force_for_new_devices: true,
    )
    automatic_code_signing(
      use_automatic_signing: false
    )
    update_project_provisioning(
      profile: ENV["sigh_com.flutter.flutterflavors.test_adhoc_profile-path"],
      build_configuration: "Release-tst",
      code_signing_identity: "iPhone Distribution"
    )
    build_app(
      scheme: "tst",
      configuration: "Release-tst",
      xcargs: "-allowProvisioningUpdates",
      export_options: {
        signingStyle: "manual",
        method: "ad-hoc",
        provisioningProfiles: {
          "com.flutter.flutterflavors.test": "match AdHoc com.flutter.flutterflavors.test",
        }
      },
      output_name: "Runner.ipa"
    )
    # upload to AppCenter or anywhere else
  end

  desc "Deploy a new version to the AppStore"
  lane :prod do

  end
end
```

To build the app with fastlane you should execute just:

```
bundle exec fastlane ios test
```

At this point you may encounter a very nasty error that fastlane tries to build `com.flutter.flutterflavors.dev` instead of `com.flutter.flutterflavors.test`:

![❌ error: Provisioning profile “match AdHoc com.flutter.flutterflavors.test” has app ID “com.flutter.flutterflavors.test”, which does not match the bundle ID “com.flutter.flutterflavors.dev”. (in target ‘Runner’)](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/error-signing.png)

The simplest solution that took me hours to find was just to delete bundle id from General tab in Xcode.

![iOS Xcode delete bundle id](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/ios-xcode-delete.gif)

See the [mp4](https://roszkowski.dev/assets/images/ios-xcode-delete.mp4)/[webm](https://roszkowski.dev/assets/images/ios-xcode-delete.webm)

Now you should be able to have you `.ipa` archive ready to submit to AppCenter, Beta or directly to your testers.

![](https://roszkowski.dev/images/2019-07-15-flutter-guide-flavors/success.png)

Take a look at the [commit a3c5512a](https://github.com/orestesgaolin/flutter-flavors/commit/a3c5512af215e48a9e38912c073ec4b79b41831b) to look through all the changes related to fastlane.

## Summary

After reading this article you should be able to configure Flutter flavors on your own. There are almost limitless possibilities related to flavors, schemes and configurations. For instance you can have separate Google Services files or Facebook ids for each flavor. You can enable or disable some features for test builds. You can even create multiple apps from single code base.

I hope you learned something with me. See you soon in the next blog post 🖖.


---
[^1]: Of course you can define as many flavors as you wish, 3 flavors are a good compromise
[^2]: On Android you can't define _test_ flavor so we named it _tst_, but we wanted _.test_ suffix to make it more obvious for QA. You can go with _test_ names and files all the way if you prefer it.
[^3]: This may be changed in future Flutter versions
[^4]: I use several plugins to fastlane like [badge](https://github.com/HazAT/fastlane-plugin-badge) or [appcenter](https://github.com/microsoft/fastlane-plugin-appcenter). I really recommend you to check them out.