iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🍨

[Flutter] Setting Up Development Environments Using flutter_flavorizr

に公開

Introduction

When I was developing personal projects, I didn't pay much attention to this, but in actual application development, APIs and servers are often developed across different environments such as development, testing, and production.
In such cases, the app side also needs to be built according to each environment, and the officially recommended method for this is to configure settings per OS using Flavor.

https://docs.flutter.dev/deployment/flavors

However, setting this up can be quite a hassle.
For example, you need to modify info.plist and schemes for iOS and macOS, and build.gradle and AndroidManifest.xml for Android.
Setting all of these up from scratch can be quite time-consuming, right?

That's why I'd like to introduce flutter_flavorizr, a package that allows you to set up environments relatively easily.

https://pub.dev/packages/flutter_flavorizr

Nonetheless, it won't work properly unless you follow the steps carefully (it took me 3 days to figure it out 🥲), so I'll leave this as a memo for myself.

Target Audience

  • Those who want to separate development environments using Flavor
  • Those who want to separate development environments using flutter_flavorizr
  • Those whose development device is macOS
  • Those who use VSCode as their IDE

Author's Environment at the time of writing

[✓] Flutter (Channel stable, 3.27.1, on macOS 15.1 24B2082 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.96.2)

Sample Project

From left:

  • iOS
  • iPad
  • MacOS

Android

  • Changes the color and text of the left sidebar based on the development environment.
  • Displays different app names at the top center and center of the screen.

Source Code

https://github.com/HaruhikoMotokawa/sample_flutter_flavorizr

Prerequisites

How does this package construct Flavor, as explained earlier? It automatically rewrites codes like info.plist and schemes for iOS, and build.gradle and AndroidManifest.xml for Android, using scripts.
Therefore, it is recommended to use it for newly created projects as much as possible.
It's not impossible to use it for existing projects, but there's a risk that settings related to Firebase or device access that you've already written in info.plist or AndroidManifest.xml might be deleted.
Be careful about that.

1. Install 3 tools

Before using this package, there are three necessary tools. These are:

  1. Ruby
  2. gem
  3. xcodeproj

Ruby is a programming language. If you're on a Mac, it should be pre-installed.
As for gem, if you're already doing Flutter development, it's likely installed.
xcodeproj is probably not installed, so you should install it.

First, let's check if each of them is installed by entering the following commands in order.

ruby --version
gem --version
xcodeproj --version

By the way, my environment is as follows:

1-1. Install Ruby

https://www.ruby-lang.org/en/documentation/installation/

macOS usually comes with Ruby pre-installed, but if you want to use the latest version or manage versions, we recommend installing it using Homebrew.

Install it with the following command:

brew install ruby

Next, set the PATH to use Ruby installed via Homebrew.

echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

As a precaution, close the current terminal and restart VSCode.

1-2. Install gem

https://rubygems.org/pages/download

When Ruby is installed, Gem is usually installed automatically. However, please check the version and update it if necessary.

gem --version
gem update --system

1-3. Install Xcodeproj

Xcodeproj is provided as a Ruby Gem, so install it using the following steps:

sudo gem install xcodeproj

https://github.com/CocoaPods/Xcodeproj

2. Prepare Podfile

After creating a new app, first prepare the Podfile.
Newly created projects don't have a Podfile, so create one.

2-1. Prepare Podfile for iOS

Navigate to the ios directory in the terminal and create a new Podfile.

cd ios
pod init

Next, copy all the following files and overwrite the Podfile you just created with them.

Podfile for iOS
# Uncomment this line to define a global platform for your project
platform :ios, '15.4'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug-prod' => :debug,
  'Profile-prod' => :release,
  'Release-prod' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end

Save it 👈 This is important!!

Run the install command to generate Podfile.lock.

pod install

Finished result

2-2. Prepare Podfile for MacOS

Navigate to the macos directory in the terminal and create a new Podfile.

cd macos
pod init

Next, copy all the following files and overwrite the Podfile you just created with them.

Podfile for MacOS
platform :osx, '10.14'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug-prod' => :debug,
  'Profile-prod' => :release,
  'Release-prod' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_macos_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_macos_build_settings(target)
  end
end

Save it 👈 This is important!!

Run the install command to generate Podfile.lock.

pod install

Finished result

3. Prepare flutter_flavorizr

3-1. Install flutter_flavorizr

It's fine to use the command line, or you can add it to pubspec.yaml.
For VSCode users, the following steps are easier:

  1. Open Command Palette (⌘ + Shift + P)
  2. Type and select dart add dev_dependencies
  3. Type and select flutter_flavorizr

If it has been added to pubspec.yaml, the installation is complete.

3-2. Create flavorizr.yaml

Create a file named flavorizr.yaml in the project root.
Copy and paste the entire content below into it and save it.
Details will be explained later, but please edit the content as needed for your personal configuration.

Full `flavorizr.yaml`
# Firebase config is currently commented out as it's not in use

# After configuration, run the following:
# flutter pub run flutter_flavorizr
# You can choose VSCode or AndroidStudio
ide: "vscode"
flavors:
  prod:
    app:
      name: "Sample Flutter Flavorizr"
    android:
      applicationId: "com.sample.flutter.flavorizr.prod"
      # firebase:
      #   config: "lib/core/firebase/prod/google-services.json"
    ios:
      bundleId: "com.sample.flutter.flavorizr.prod"
      # firebase:
      #   config: "lib/core/firebase/prod/GoogleService-Info.plist"
    macos:
      bundleId: "com.sample.flutter.flavorizr.prod"
      # firebase:
      #   config: "lib/core/firebase/prod/GoogleService-Info.plist"
  stg:
    app:
      name: "Sample Flutter Flavorizr Stg"
    android:
      applicationId: "com.sample.flutter.flavorizr.stg"
      # firebase:
      #   config: "lib/core/firebase/stg/google-services.json"
    ios:
      bundleId: "com.sample.flutter.flavorizr.stg"
      # firebase:
      #   config: "lib/core/firebase/stg/GoogleService-Info.plist"
    macos:
      bundleId: "com.sample.flutter.flavorizr.stg"
      # firebase:
      #   config: "lib/core/firebase/stg/GoogleService-Info.plist"
  dev:
    app:
      name: "Sample Flutter Flavorizr Dev"
    android:
      applicationId: "com.sample.flutter.flavorizr.dev"
      # firebase:
      #   config: "lib/core/firebase/dev/google-services.json"
    ios:
      bundleId: "com.sample.flutter.flavorizr.dev"
      # firebase:
      #   config: "lib/core/firebase/dev/GoogleService-Info.plist"
    macos:
      bundleId: "com.sample.flutter.flavorizr.dev"
      # firebase:
      #   config: "lib/core/firebase/dev/GoogleService-Info.plist"
# INFO: Comment out unnecessary items
# instructions:
# - assets:download
# - assets:extract
# - android:androidManifest
# - android:buildGradle
# - android:dummyAssets
# - android:icons
# - flutter:flavors
# - flutter:app
# - flutter:pages
# - flutter:main
# - flutter:targets
# - ios:podfile
# - ios:xcconfig
# - ios:buildTargets
# - ios:schema
# - ios:dummyAssets
# - ios:icons
# - ios:plist
# - ios:launchScreen
# - macos:podfile
# - macos:xcconfig
# - macos:configs
# - macos:buildTargets
# - macos:schema
# - macos:dummyAssets
# - macos:icons
# - macos:plist
# - google:firebase
# - huawei:agconnect
# - assets:clean
# - ide:config

About flavors

Create names as you like. I created the following three:

  • dev :develop / for development
  • stg :staging / for testing
  • prod :production / for production

Then, assign an app name and OS-specific bundle ID to each created flavor.
Below is an example for prod.

  prod:
    app:
      name: "Sample Flutter Flavorizr"
    android:
      applicationId: "com.sample.flutter.flavorizr.prod"
      # firebase:
      #   config: "lib/core/firebase/prod/google-services.json"
    ios:
      bundleId: "com.sample.flutter.flavorizr.prod"
      # firebase:
      #   config: "lib/core/firebase/prod/GoogleService-Info.plist"
    macos:
      bundleId: "com.sample.flutter.flavorizr.prod"
      # firebase:
      #   config: "lib/core/firebase/prod/GoogleService-Info.plist"

Firebase settings are commented out because I haven't configured them. If you are setting them up, describe the necessary paths here. Please note that this guide does not cover how to set up Firebase for each environment.

About instructions

Although this part is commented out in the current YAML, only the items explicitly listed in instructions will be configured.
This means that items listed by default will be configured.

For example, if you don't develop for macOS and want to exclude its settings, you can do so as follows:

instructions:
- assets:download
- assets:extract
- android:androidManifest
- android:buildGradle
- android:dummyAssets
- android:icons
- flutter:flavors
- flutter:app
- flutter:pages
- flutter:main
- flutter:targets
- ios:podfile
- ios:xcconfig
- ios:buildTargets
- ios:schema
- ios:dummyAssets
- ios:icons
- ios:plist
- ios:launchScreen
# - macos:podfile
# - macos:xcconfig
# - macos:configs
# - macos:buildTargets
# - macos:schema
# - macos:dummyAssets
# - macos:icons
# - macos:plist
- google:firebase
- huawei:agconnect
- assets:clean
- ide:config

For details on each setting, please refer to the official documentation.

https://pub.dev/packages/flutter_flavorizr

4. Run and Adjust Script

4-1. Execute and Confirm Script

Execute the script by running the following command in the terminal.

flutter pub run flutter_flavorizr

The script will start running, and the terminal will show a lot of activity.
Once all processes are complete, check if the app can be launched for each OS with the configured environment.
If there are no configuration errors, all should launch except for macOS at this point.

macOS will be explained in 4-3. Adjusting macOS

  1. Launch Command Palette (⌘ + Shift + P)
  2. Type Select Device and choose the device to launch
  3. Tap the launch button on the activity bar
  4. Expand the collapse arrow to the left of "Run and Debug"
  5. Select the desired Flavor from the displayed options
  6. Tap the green run button
  7. As a precaution, also check the debug console

4-2. Adjust launch.json

After running the script, various files are generated, and launch.json is one of them.
It is generated inside the .vscode directory at the root of your project.
If you look inside, it will initially be on a single line and unformatted. If you have automatic formatting on save enabled in VSCode settings, save the file.

For each Flavor, three different flutterMode build modes are assigned:

  • debug
  • profile
  • release

During development, you'll generally only use debug, so you can comment out the others and move the necessary ones to the top.
Personally, I prefer commenting them out rather than deleting them, as it makes them easily accessible if other execution modes are needed later.
Feel free to adjust this to your preference.

.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "dev Debug",
            "request": "launch",
            "type": "dart",
            "flutterMode": "debug",
            "args": [
                "--flavor",
                "dev"
            ],
            "program": "lib/flavors/main_dev.dart"
        },
        {
            "name": "stg Debug",
            "request": "launch",
            "type": "dart",
            "flutterMode": "debug",
            "args": [
                "--flavor",
                "stg"
            ],
            "program": "lib/flavors/main_stg.dart"
        },
        {
            "name": "prod Debug",
            "request": "launch",
            "type": "dart",
            "flutterMode": "debug",
            "args": [
                "--flavor",
                "prod"
            ],
            "program": "lib/flavors/main_prod.dart"
        },
        // {
        //     "name": "prod Profile",
        //     "request": "launch",
        //     "type": "dart",
        //     "flutterMode": "profile",
        //     "args": [
        //         "--flavor",
        //         "prod"
        //     ],
        //     "program": "lib/main_prod.dart"
        // },

        // Ellipsis below
    ]
}

4-3. Adjusting MacOS

At this point, macOS might not launch for some reason.
This is mentioned in the troubleshooting section of flutter_flavorizr's GitHub repository: it won't work correctly if the app's Minimum Deployments (the minimum guaranteed version for the app) is less than 11.

https://github.com/AngeloAvv/flutter_flavorizr/blob/master/doc/troubleshooting/unable-to-load-contents-of-file-list/README.md#macos

Therefore, we need to edit the settings to fix this.
This time, let's set it to macOS 12.4 or higher.

Launch Xcode from the command line.

open macos/Runner.xcodeproj
  1. Select Runner
  2. Select Runner under TARGETS
  3. Select General
  4. Tap the three dots to the right of Minimum Deployments
  5. Set the target for all Flavors to 11 or higher (in this case, set to 12.4)

With this, macOS will also be able to build per Flavor.

4-4. Explanation of Automatically Generated Dart Files

Files automatically generated after running the script.

flavors.dart

This defines an enum for the configured Flavor types and an F class that takes and utilizes this enum as an argument.

lib/flavors.dart
enum Flavor {
  prod,
  stg,
  dev,
}

class F {
  static Flavor? appFlavor;

  static String get name => appFlavor?.name ?? '';

  static String get title {
    switch (appFlavor) {
      case Flavor.prod:
        return 'Sample Flutter Flavorizr';
      case Flavor.stg:
        return 'Sample Flutter Flavorizr Stg';
      case Flavor.dev:
        return 'Sample Flutter Flavorizr Dev';
      default:
        return 'title';
    }
  }
}

main_**.dart

Generated for each Flavor.
The content is simple: it assigns the Flavor at build time to the appFlavor property of the F class.
The build is separated because the path to be executed for each Flavor is specified in launch.json, as explained earlier.

The runner.main() in the function executes the main function of the original main.dart.

lib/flavors/flavors.dart
Future<void> main() async {
  F.appFlavor = Flavor.dev;
  await runner.main();
}

my_home_page.dart

This is an example page for usage.
Here, the displayed string changes depending on the Flavor. You can edit or delete it if it's no longer needed.

lib/pages/my_home_page.dart
        child: Text(
          'Hello ${Flavor.title}',
        ),

app.dart

This file contains logic to display different banner colors and text based on the Flavor.
This is just a sample, so it can be modified.

`app.dart`
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: F.title,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: _flavorBanner(
        child: MyHomePage(),
        show: kDebugMode,
      ),
    );
  }

  Widget _flavorBanner({
    required Widget child,
    bool show = true,
  }) =>
      show
          ? Banner(
              location: BannerLocation.topStart,
              message: F.name,
              color: Colors.green.withOpacity(0.6),
              textStyle: TextStyle(
                  fontWeight: FontWeight.w700,
                  fontSize: 12.0,
                  letterSpacing: 1.0),
              textDirection: TextDirection.ltr,
              child: child,
            )
          : Container(
              child: child,
            );
}

Summary of points to note when changing code

Items that can be deleted

  • my_home_page.dart

Items whose directory can be moved, functions can be deleted or added

  • main.dart
  • app.dart

Items whose directory can be moved, renamed or extended

  • flavors.dart

Items whose directory must not be moved

  • main_**.dart

5. Bonus: For those who want a little customization

From here, I'll introduce how I customized things because I was curious.

5-1. Renaming and Extending Classes

First, I renamed the enum and class because their naming didn't feel right to me.

  • enum Flavorenum FlavorType
  • class Fclass Flavor

Next, since it will often be necessary to differentiate processing based on the current build's FlavorType,
I also define bool values for judging this.

The overall result is as follows:

`flavors.dart`
// Renamed
enum FlavorType {
  prod,
  stg,
  dev,
}

// Renamed
class Flavor {
  static FlavorType? appFlavor;

  static String get name => appFlavor?.name ?? '';

  // bool to determine if it's each type
  static bool get isProd => appFlavor == FlavorType.prod;
  static bool get isStg => appFlavor == FlavorType.stg;
  static bool get isDev => appFlavor == FlavorType.dev;

  static String get title {
    switch (appFlavor) {
      case FlavorType.prod:
        return 'Sample Flutter Flavorizr';
      case FlavorType.stg:
        return 'Sample Flutter Flavorizr Stg';
      case FlavorType.dev:
        return 'Sample Flutter Flavorizr Dev';
      default:
        return 'title';
    }
  }

  // A getter I created for testing, can be customized as desired
  static Color get color {
    switch (appFlavor) {
      case FlavorType.prod:
        return Colors.green.withValues(alpha: 0.6);
      case FlavorType.stg:
        return Colors.blue.withValues(alpha: 0.6);
      case FlavorType.dev:
        return Colors.yellow.withValues(alpha: 0.6);
      default:
        // withOpacity is deprecated since Flutter 3.27.0, so use withValues(alpha:)
        return Colors.grey.withValues(alpha: 0.6);
    }
  }
}

5-2. Moving main_**.dart Directories

Personally, I thought it would be better to keep main_**.dart and flavors in a flavors directory.

However, as explained earlier, this alone will prevent the build from succeeding.
Therefore, two main corrections are needed.

Correct launch.json

We will rewrite each execution path to the changed lib/flavors/main_**.dart.
In this case, there are 3: dev, stg, prod, but you need to do this for as many Flavors as you have configured.

Correct **.xcconfig for iOS

We will rewrite the FLUTTER_TARGET path in the .xcconfig files inside ios/Flutter.

In this case, I modified all 9 files highlighted in yellow in the image below, including modes that were not in use, just in case.

Correct **.xcconfig for MacOS

Note that for MacOS, you will be modifying files in a different directory than for iOS.
The location is inside macos/Runner/Configs.

In this case, I also modified all 9 files highlighted in yellow in the image below, including modes that were not in use, just in case.

Good job!
With these corrections, the build should now work.

Conclusion

In this article, I introduced how to efficiently set up environments in a Flutter project using flutter_flavorizr.
By leveraging flutter_flavorizr, you can easily manage and switch between multiple environments such as development, testing, and production, compared to manual setup.

If you find any mistakes or areas for improvement in the article, please let me know in the comments.

Flavor configuration is something you set up once for a project and then tend to forget because you don't touch it often.
I hope this article helps in setting up environments for Flutter projects.

References

https://blog.mrym.tv/2021/11/flutter_flavor_setting_using_flutter_flavorizr/

https://zenn.dev/masa_tokyo/articles/flutterfire-cli-flavor#参考記事

Discussion