混乱しがちなFlutterのビルド構成を整理しよう
混乱しがちな Flutter のビルド構成
Flutter でモバイルアプリを実行するとき、次のようなコマンドを目にすることが多いはずです。
flutter run \
--debug \
--flavor dev \
--dart-define-from-file=env/dev.json
これらのオプションはそれぞれ次の通りです。きっと大まかには知っていますよね👀
オプション | サンプルコマンドの要素 |
---|---|
Build Mode | --debug |
Flavor | --flavor dev |
dart‑define-from-file | --dart-define-from-file=env/dev.json |
しかし、いざ詳しく説明しようとすると詰まってしまうことはないでしょうか?
そんな私みたいな人のために Flutter のビルド構成を改めて整理してみます。
Build Mode
👉 指定するもの:Flutter コードを「どう」動かすか
👉 ざっくりいうと:同じアプリでも「JIT でサクサク開発」か「AOT で本番」かを切り替える
Build Mode はアプリの Flutter 部分の実行方式を指定するものです。
たとえばデバッグビルドの場合、Flutter では JIT コンパイラが選択され、ホットリロードが可能になります。
一方でリリースビルドの場合は AOT が採用され、アプリ全体のサイズが最適化される、という具合ですね。
CLI フラグ | ざっくり用途 | 特徴 |
---|---|---|
--debug |
開発したい、デバッグしたい | JIT / assert 有効 / ホットリロード / DevTools 可 |
--profile |
パフォーマンスを計測したい | AOT / assert 無効 / ホットリロード不可 / DevTools 可 / 最小限のログ |
--release |
ストアに提出する最終ビルド | AOT / assert 無効 / デバッグ機能皆無 / バイナリ最小化 |
詳細は次のページから確認できます。
参考:JIT と AOT
Flutter では debug ビルドでは JIT, リリースビルド時は AOT という仕組みが採用されています。
Release, Profile ビルドでは Dart コードは AOT でネイティブ命令に変換され、(Dart VM を除く) Flutter エンジンとともに apk, ipa にパッケージされます。
- JIT (Just-In-Time)
- コードを実行中に逐次コンパイルする
- ホットリロードが可能で、修正したコードを即座に反映できる
- AOT (Ahead-Of-Time)
- コードを事前にすべてコンパイルする
- 高速な実行が可能であり、パフォーマンスに優れる
Flavor
👉 指定するもの:どの環境で動くアプリか
👉 ざっくりいうと:ネイティブが識別できる BundleID/アイコン/署名等を差し替える仕組み
Flavor は dev
, staging
, prod
など、環境ごとに自由に名前を付けられます。
Build Mode と異なり、これはネイティブ環境 (iOS/Android) の設定です。
Flutter はクロスプラットフォーム層であり、実際の実行バイナリはネイティブ環境でコンパイルされますね。
そのネイティブビルド時のオプションを指定する仕組みが Flavor です。
例えば、モバイルアプリのビルドにはアプリ固有の ID が必須です。
同じ ID のアプリは端末に 1 つしかインストールできませんが、Flavor 毎に ID を分けることで同じ端末に複数環境のアプリを共存させる、みたいなことができるわけですね。
Flavor の実態はネイティブビルド環境で以下のように定義されています。
OS | Flavor の正体 | よく変えるもの |
---|---|---|
Android |
productFlavors (Gradle) |
applicationId、アプリアイコン、Firebase 設定など |
iOS | Target + Build Configuration (Xcode) | Bundle Identifier、Team ID、Deep Link URL Scheme など |
バンドル ID 以外にも、アプリアイコンや Deep Link ホストなど色々切り替える事が可能です。
dart‑define / dart‑define-from-file
👉 指定するもの:Dart コードに渡す定数
👉 ざっくりいうと:ソースに直書きしたくない URL や API キーをビルド時に注入
開発環境と本番環境で接続先のサーバを切り替えたり、機密情報 (API キーなど) を安全に管理するために使います。
コード内に直接記述するとセキュリティリスクがある情報をビルド時にのみ注入します。
実務上では GitHub Actions や CI/CD ツールで自動化し、安全かつ効率的に設定するのが一般的でしょう。
--dart-define
, --dart-define-from-file
は渡せる変数の数に違いがあります。
コマンド | 件数 |
---|---|
--dart-define=KEY=VAL |
一件のみ |
--dart-define-from-file=env.json |
JSON ファイル内の複数変数をまとめて指定 |
ここまでの違いをもう一度整理
以上の内容を整理しておきましょう。
仕組み | 変わるもの | 決定タイミング | 例 |
---|---|---|---|
Build Mode | コンパイル方式 / デバッグ機能 |
flutter run / flutter build 実行時 |
debug , profile , release
|
Flavor | バンドル ID・アイコンなどネイティブ設定 | Gradle, Xcode ビルド開始時 |
dev , prod
|
dart‑define | Dart で読む定数 | 同上 | API_BASE=https://… |
以上を踏まえると、たとえば次のようなサンプルコマンドが浮かびます。
# ローカル開発 (Debug + Dev Flavor)
flutter run \
--debug \
--flavor dev \
--dart-define=FLAVOR=dev \
--dart-define-from-file=env/dev.json
# 本番リリース (Release + Prod Flavor)
flutter build ios \
--release \
--flavor prod \
--dart-define-from-file=env/prod.json
これらのオプションは役割がかぶらないので遠慮なく指定して OK ですが、実際の運用では
-
--debug
,dev
,env/dev.json
-
--release
,prod
,env/prod.json
のように意味が重複したオプションが並ぶことになります。
ビルドコマンドは繰り返し入力するものなので、Makefile や melos スクリプトに登録して置くと GOOD ですね!
# Makefile
dev:
flutter run --debug --flavor dev --dart-define-from-file=env/dev.json
prod:
flutter build ios --release --flavor prod --dart-define-from-file=env/prod.json
# melos.yaml
scripts:
dev: flutter run --debug --flavor dev --dart-define-from-file=env/dev.json
prod: flutter build ios --release --flavor prod --dart-define-from-file=env/prod.json
感想
整理してみると、やはり混乱の原因は Flutter がクロスプラットフォームであることなのかなと感じます。
テーブルで整理すると次のような形ですね。
レイヤー | 主たるフラグや概念 | 何を制御するか |
---|---|---|
Dart + エンジン層 | Build Mode | Debug ビルドでは JIT + Dart VM 同梱 Release/Profile ビルドでは AOT & VM なし |
Dart 定数層 | dart‑define | ビルド時に定数値を設定 |
ネイティブ層 | Flavor | Gradle/Xcode で applicationId, バンドル ID, 署名などを環境別に差し替え |
このように 1 本のflutter
コマンドに 3 つレイヤーが混在していると、初学者は嫌でも混乱します…。
流行りの Vibe Coding も楽しいですが、Flutter アプリ開発では、ネイティブ設定とクロスプラットフォーム層の役割分担をちゃんと理解しておかないとですね。
Discussion