🍨

【Flutter】flutter_flavorizrを使って環境構築をしてみた

に公開

はじめに

個人開発の時はあまり気にしなかったことなのですが、実務でのアプリケーション開発においてAPIやサーバーなどを開発環境、テスト環境、本番環境というように分けて開発しています。
その場合、アプリ側もその環境に合わせたビルドを行う必要があり、その方法としては公式が推奨しているFlavorを使ってOSごとに設定をしなければいけません。

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

しかし、その設定にはなかなか手間がかかります。
例えばiOSやmacOSのinfo.plistたscheme、Androidではbuild.gradleとAndroidManifest.xmlなどに手を加える必要があります。
これらを一から設定するのはなかなか手間がかかりますよね?

そこで今回紹介するflutter_flavorizrというパッケージを使えば比較的楽に環境を構築することができるのでご紹介します。

https://pub.dev/packages/flutter_flavorizr

とはいえ、しっかり手順を踏まないとうまく動かない(私は3日悩みました🥲)ので自分のためにも備忘録として残しておこうかと思います。

記事の対象者

  • Flavorを使って開発環境を分けたい方
  • flutter_flavorizrを使って開発環境を分けたい方
  • 開発デバイスがMacOSの方
  • IDEをVScodeを使用している方

記事を執筆時点での筆者の環境

[✓] 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)

サンプルプロジェクト

左から

  • iOS
  • iPad
  • MacOS

Android

  • 開発環境によって画面左側の帯の色と帯の文字を変更
  • 画面中央上部や中央のアプリ名を異なる表記で表示

ソースコード

https://github.com/HaruhikoMotokawa/sample_flutter_flavorizr

前提

このパッケージはどう言った仕組みでFlavorを構築しているかというと、先にも説明したinfo.plistやscheme、Androidではbuild.gradleとAndroidManifest.xmlなどのコードをスクリプトで自動で書き換えています。
なので、できるだけ新規作成したばかりのプロジェクトに使用することをおすすめします。
既存のプロジェクトに絶対に使えないわけではないのですが、info.plistやAndroidManifest.xmlなどにFirebase周りやデバイスのアクセス周りの設定をすでに書き込んでいる場合に消されてしまうリスクがあります。
その辺には注意しましょう。

1. 3つのツールをインストールする

このパッケージを使用する前に必要なツールが3つあります。それが次の3つです。

  1. Ruby
  2. gem
  3. xcodeproj

Rubyはプログラミング言語です。macであれば標準で入っているはずです。
gemに関してすでにFlutter開発をしている人であれば入っている可能性が高いです。
xcodeprojはおそらく入っていない可能性が高いので入れましょう。

まずはそれぞれがインストールされてるかを以下のコマンドを順番に入力して確認をしましょう。

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

ちなみに私の環境は以下です。

1-1. Rubyのインストール

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

macOSには通常Rubyがプリインストールされていますが、最新バージョンを使用したい場合やバージョン管理を行いたい場合は、Homebrewを使用してインストールすることをおすすめします。

以下のコマンドでインストールしましょう。

brew install ruby

次にHomeBrew経由でインストールしたRubyを使うようにPathを通します。

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

念のため、現在のターミナルを破棄して、VScodeを再起動します。

1-2. gemのインストール

https://rubygems.org/pages/download

Rubyをインストールすると、通常Gemも自動的にインストールされます。しかし、念のためバージョンを確認し、必要に応じてアップデートしてください。

gem --version
gem update --system

1-3. Xcodeprojのインストール

https://github.com/CocoaPods/Xcodeproj

Xcodeproj はRubyのGemとして提供されているため、以下の手順でインストールします。

sudo gem install xcodeproj

2. Podfileの準備

新規アプリを作成したらまずはPodfileの準備を行います。
作成されたばかりのプロジェクトにはPodfileがないので作成します。

2-1. iOSのPodfileの準備

ターミナルでiosディレクトリに移動し、Podfileを新規作成します。

cd ios
pod init

次に以下のファイルをすべてコピーし、そのまま先ほど作ったPodfileにすべて上書きします。

iOS用のPodfile
# 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

保存します👈ここ大事!!

インストールコマンドを叩いてPodfile.lockを生成します。

pod install

完成図

2-2. MacOSのPodfileの準備

ターミナルでmacosディレクトリに移動し、Podfileを新規作成します。

cd macos
pod init

次に以下のファイルをすべてコピーし、そのまま先ほど作ったPodfileにすべて上書きします。

MacOS用のPodfile
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

保存します 👈ここ大事!!

インストールコマンドを叩いてPodfile.lockを生成します。

pod install

完成図

3. flutter_flavorizrの準備

3-1. flutter_flavorizrのインストール

コマンドラインでもいいですし、pubspec.yamlに入力でもOKです。
VSCodeの方は次の手順が簡単です。

  1. コマンドパレット展開(⌘ + shift + P)
  2. dart add dev_dependenciesを入力して選択
  3. flutter_flavorizrを入力して選択

pubspec.yamlに追加されていればインストール完了です。

3-2. flavorizr.yamlを作成

プロジェクトのルートにflavorizr.yamlという命名でファイルを作成します。
その中に以下を全文コピペして保存してください。
詳細は後述しますが、以下の設定内容から個人ごとに必要な内容を編集してください。

flavorizr.yamlの全文
# Firebaseのconfigは現在未使用のためコメントアウトしている

# 設定が終わったら以下を実行
# flutter pub run flutter_flavorizr
# VSCodeか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: 不要なものはコメントアウトする
# 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

flavorsについて

任意の命名で作成します。私の場合は以下の3つを作成しました。

  • dev :develop / 開発用
  • stg :staging / テスト用
  • prod :production / 本番用

そして作成したそれぞれのフレーバにアプリ名とos事のバンドルIDを割り当てていきます。
以下は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の設定はしていないのでコメントアウトしています。
設定される方はここで必要なパスを記述します。
Firebaseを環境ごとに設定する方法は今回説明していないので、ご了承ください。

instructionsについて

今回のyamlにはコメントアウトしている部分ですが、instructionsの中に記述があるものだけ設定するようになります。
つまり列挙してあるものはデフォルトでは設定されることになります。

例えばここでmacosは開発しないので設定を外したい場合は以下のようにすると設定されなくなります。

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

それぞれの設定詳細は公式のドキュメントに説明があるので、ご確認ください。

https://pub.dev/packages/flutter_flavorizr

4. スクリプトの実行と調整

4-1. スクリプト実行と確認

ターミナルで以下のコマンドを叩いてスクリプトを実行します。

flutter pub run flutter_flavorizr

するとスクリプトが走ってターミナルがザーっと動きはじます。
すべての工程が完了したのを確認したら設定した環境でアプリを立ち上げられるかOSごとに確認をしましょう。
設定さえ間違っていなかればこの時点ではMacOS以外は立ち上がるはずです。

macosについては4-3. MacOSの調整で解説します

  1. コマンドパレッド起動( ⌘ + shift + P )
  2. Select Deviceを入力して起動するデバイスを選択
  3. アクティビティバーの起動ボタンをタップ
  4. 実行とデバックの左側の折りたたみを展開
  5. 表示されている中から任意のFlavorを選択
  6. 緑の実行ボタンをタップ
  7. 念のため、デバックコンソールでも確認

4-2. launch.jsonの調整

スクリプト実行後にはさまざまなファイルが生成されますが、そのうちの一つにlaunch.jsonがあります。
生成場所はルート配下の.vscodeディレクトリ内です。
中を覗くと最初は整形されていない状態で1行になっています。VScodeの設定でファイル保存による自動整形をオンにしている方は保存しましょう。

一つのFavorに対して異なる3つのflutterModeが割り当てられたビルドモードが設定されています。

  • debug
  • profile
  • release

この中で基本的に開発段階ではdebugしか使わないと思うので、それ以外はコメントアウトして必要なものを上に持ってきてしまいます。
個人的にはコード削除よりはもしも他の実行モードが必要になった場合にすぐに使えるようにしておいた方がいいと思うので、コメントアウト派です。
ここら辺は各個人お好みで調整してください。

.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"
        // },

        // 以下省略
    ]
}

4-3. MacOSの調整

ここまできた段階で、なぜかMacOSだけは立ち上がりません。
これはflutter_flavorizrのGitHubのリポジトリ内にあるトラブルシューティングに記載があるのですが、アプリのMinimum Deployments(開発するアプリの最低保証バージョン)が11未満だとうまく動かないようです。

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

そこで、設定を編集して修正します。
今回はmacos12.4以上にしてみます。

コマンドラインでXcodeを立ち上げます。

open macos/Runner.xcodeproj
  1. Runnerを選択
  2. TARGETSのRunnerを選択
  3. Generalを選択
  4. Minimum Deploymentsの右にある3点リーダーをタップ
  5. すべてのFlavorの対象を11以上にする(今回は12.4に設定)

これで無事にMacOSもFlavorごとにビルドできるようになります。

4-4. 自動生成されたdartファイルの解説

スクリプトを走らせたことによって自動生成されたファイル達があります。

flavors.dart

設定したFlavorの種類を定義したenumとそのenumを引数にとって保持し、活用するFクラスが定義されています。

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

Flavorごとに生成されます。
中身はシンプルでFクラスのappFlavorプロパティにビルドされた時のFlavorを代入しています。
先に説明したlaunch.jsonのFlavor事に実行するパスが指定されることで切り分けることができています。

関数内に記述があるrunner.main()は元々あったmain.dartのmain関数を実行しています。

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

my_home_page.dart

使用例としての見本ページです。
ここではFlavorによって表示する文字列を変えています。不要になったら編集、削除して構いません。

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

app.dart

この中ではFlavorによってバナーの色や文字を出し分ける処理が書かれています。
ここは見本を見せているだけなので、変更可能です。

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

  
  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,
            );
}

コードを変更する際の注意点まとめ

削除して良いもの

  • my_home_page.dart

ディレクトリの移動、関数内の削除、または追加してもいいもの

  • main.dart
  • app.dart

ディレクトリの移動、リネームや拡張をしてもいいもの

  • flavors.dart

ディレクトリの移動をしてはいけないもの

  • main_**.dart

5.番外編: ちょっとしたカスタマイズをしたい方へ

ここからは私個人としては気になったのでカスタマイズした際の方法をご紹介します。

5-1. クラス名の変更とクラスの拡張

まず、enumとclassの命名が私としてはしっくりこないのでリネームしました。

  • enum Flavorenum FlavorType
  • class F -> class Flavor

次に現在のビルドがどのFlavorTypeかによって処理を切り分けることが多々あると思うので、
判定するためのbool値も定義してあげます。

そうした場合の全体が以下のような形です。

flavors.dart
// リネーム
enum FlavorType {
  prod,
  stg,
  dev,
}

// リネーム
class Flavor {
  static FlavorType? appFlavor;

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

  // それぞれのタイプかどうかの判定を返すbool
  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';
    }
  }

  // 今回試しに作ったgetter、好きに作れる
  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はFlutter3.27.0以降で非推奨になったので、withValues(alpha:)を使う
        return Colors.grey.withValues(alpha: 0.6);
    }
  }
}

5-2. main_**.dartのディレクトリを移動する

個人的にはmain_**.dartとflavorsはflavorsディレクトリを作ってしまっておきたいと考えました。

ただしこのままだと先にも説明したようにビルドが通らなくなってしまいます。
そこで、おおまかに2つの修正が必要です。

launch.jsonの修正

それぞれの実行pathを変更したlib/flavors/main_**.dartに書き換えていきます。
今回はdev,stg,prodの3つですが、設定したFlavorの数だけ行う必要があります。

iOSの**.xcconfigの修正

ios/Flutter内にある.xcconfigのFLUTTER_TARGETのpathを書き換えていきます。

今回の場合は使用していないモードも含めて念のため書き換えたので、下の画像でファイル名が黄色になっている全部で9つのファイルを修正しました。

MacOSの**.xcconfigの修正

MacOSの場合はiOSの時と違うディレクトリ内のファイルを変更するので注意が必要です。
場所はmacos/Runner/Configsの中にある方です。

今回の場合も使用していないモードも含めて念のため書き換えたので、下の画像でファイル名が黄色になっている全部で9つのファイルを修正しました。

お疲れ様でした。
以上で修正されてビルドができるようになります。

終わりに

今回の記事では、flutter_flavorizrを使用してFlutterプロジェクトにおける環境構築を効率的に行う方法をご紹介しました。
手動での設定に比べて、flutter_flavorizrを活用することで、開発、テスト、本番といった複数の環境を簡単に管理・切り替えできるようになります。

もしも記事の内容に間違いや改善点がありましたらコメントで教えていただけると幸いです。

Flavorの設定はプロジェクトに対して一回設定してしまうと触る機会がないので忘れがちになってしまう項目です。
この記事が、Flutterプロジェクトにおける環境構築の一助となれば幸いです

参考記事

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

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

Discussion