🚨

混乱しがちな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