Flutter 環境分けの手法選定を考察してみた ~ --dart-define(-from-file) でネイティブ連携しない理由を添えて ~
概要
Flutterプロジェクトの環境分けの実装は様々です。
--flavorのみで環境分けしているプロジェクト--flavor+--dart-defineまたは--dart-define-from-fileを組み合わせているプロジェクトflutter_dotenvやenviedを使っているプロジェクト
それぞれの手法について、選定基準を言語化できるほど自分が理解できていなかったため、改めて調べてみました。
なお、この記事では各手法の選定基準と役割の整理を目的としており、各環境分けのセットアップ手順・実装手順は扱いません。
伝えたいこと
- 各手法(
--flavor/--dart-define/--dart-define-from-file/flutter_dotenv/envied)の役割と選定基準 --dart-define/--dart-define-from-fileでネイティブ連携するのは誤用・ハックであること
解決したい課題
「Flutterプロジェクトの環境分けはどの手法が良いの?」という疑問を持っている方に向けて、この記事で整理します。
課題の原因
Flutterの環境分けが難しい理由は主に2つです。
1. 選択肢が多い
--flavor、--dart-define、--dart-define-from-file、flutter_dotenv、envied と、環境分けを実現する手段が複数存在します。それぞれ異なる仕組みで動作するため、どれを使うべきか判断しにくいです。
2. クロスプラットフォームゆえのネイティブ連携が複雑
Flutterはクロスプラットフォームです。「Dartコードで使う値」「ネイティブ側(アプリID・アプリ名・Firebase設定)で使う値」を別々に管理する必要があり、これが混乱の元になります。
各手法の役割
まず大きく2つに分類できます。
| 分類 | 手法 |
|---|---|
| ネイティブ側(アプリID・アプリ名・Firebase設定ファイルなど) | --flavor |
| Dartコード側(APIのベースURL・フラグなど) | --flavor / --dart-define / --dart-define-from-file / flutter_dotenv / envied |
ネイティブ側とDartコード側で担当する手法が異なります。以降で各手法を詳しく解説します。
--flavor
Android の Product Flavors / iOS の Schemes を利用したネイティブレベルの環境分けです。アプリID・アプリ名・Firebase設定ファイルの切り替えなど、ネイティブ側の環境分けはこれで行います。
flutter run --flavor dev
--flavor で指定した値は Dart 側から appFlavor で参照できます。
import 'package:flutter/services.dart';
print(appFlavor); // "dev" or "prod"
appFlavor
appFlavor は packages/flutter/lib/src/services/flavor.dart に定義されている const String?(nullable なコンパイル時定数) です。
const String? appFlavor = String.fromEnvironment('FLUTTER_APP_FLAVOR') != ''
? String.fromEnvironment('FLUTTER_APP_FLAVOR')
: null;
flutter run --flavor dev→ Flutter CLI が内部で--dart-define=FLUTTER_APP_FLAVOR=devを自動追加String.fromEnvironment()は Dart コンパイラがビルド時に解決 → バイナリには'dev'が焼き込まれる--flavor未指定時は空文字''→nullに正規化--dart-define=FLUTTER_APP_FLAVOR=xxxの手動追加は Flutter CLI が予約キーとしてエラーで弾く
ランタイムの OS 環境変数を読んでいるわけではなく、あくまでビルド時に解決される定数です。
ただし 型が String?(nullable)である点 が、後述の tree-shaking の議論で重要になります。
--dart-define
Dartコードへのコンパイル時定数の注入 が目的です。ビルドコマンドに直接1つずつ値を渡し、Dart 側では String.fromEnvironment で受け取ります。設定値が少ない場合に適しています。
flutter run --dart-define=BASE_URL=https://dev.example.com
const baseUrl = String.fromEnvironment('BASE_URL');
--dart-define-from-file
--dart-define の糖衣構文です。複数の値をJSONファイルにまとめて管理でき、設定値が増えてもビルドコマンドがスッキリします。
flutter run --dart-define-from-file=dart_defines/dev.json
{
"BASE_URL": "https://dev.example.com",
"FEATURE_FLAG": "true"
}
flutter_dotenv
.env ファイルをアセットとしてバンドルし、実行時に読み込むパッケージです。扱う値はDartコード側のみで、ネイティブ側の環境分けは引き続き --flavor が担います。
--dart-define(-from-file) の代替として選ばれることが多く、採用理由は「.env ファイルで環境変数を管理する書き方に慣れているから」というケースが多いです。バックエンド開発などの経験がある方には馴染みやすい形式です。
envied
.env ファイルをもとにビルド時にコード生成を行うパッケージです。扱う値はDartコード側のみという点は flutter_dotenv と同じです。
flutter_dotenv との違いはセキュリティへの配慮です。envied はビルド時にコード生成するためバンドルされた .env ファイルが残らず、XOR 難読化オプションを使うと生成コード内の値も難読化されます。
注意点・留意点
--dart-define(-from-file) でネイティブ連携しない
--dart-define / --dart-define-from-file はDartコード専用で、ネイティブへの連携は設計外です。ネイティブ側の環境分けは --flavor を使いましょう。
かつて --dart-define-from-file でネイティブからも値を参照できる時期がありましたが、Flutter チームが「意図しない副作用」と判断しバグ修正として削除されています。この経緯を知らないと、「なぜネイティブから読めないのか」「なぜハックな実装をしているプロジェクトがあるのか」が理解しにくくなります。
- 削除: flutter/flutter#136865
- 復活要望(却下): flutter/flutter#138793
- ネイティブ向け専用オプション(
--native-defines-from-file等)は未実装・OPEN: flutter/flutter#139289
それでも値は Gradle のプロパティに base64 エンコードされた状態で渡るためデコードすれば読めますが、公式サポート外のハックであり、Flutterのバージョンアップで動作が変わるリスクがあります。
// ハックな読み方
def dartEnvironmentVariables = project.property('dart-defines')
.split(',')
.collectEntries { entry ->
def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
[(pair.first()): pair.last()]
}
秘匿情報の取り扱い
クライアントアプリに秘匿情報を持たせることは、どの手法を使っても完全には安全ではありません。秘匿情報はサーバーサイドで管理するのが原則です。
各手法のリスクは以下の通りです。
| 手法 | リスク |
|---|---|
--dart-define |
バイナリに平文で埋め込まれる。逆コンパイルで取り出せる |
flutter_dotenv |
アセットにそのまま残る。バイナリ解析で見える |
envied |
XOR難読化でカジュアルな抽出への耐性はあるが、静的・動的解析で復元可能 |
やむを得ずクライアントに秘匿情報を持たせる場合は、envied が最も耐性が高くおすすめです。
考察
環境分けは --flavor だけで十分?
--flavor を使えば appFlavor でDart側からflavor名を参照できます。
環境別の設定切り替えも tree-shaking も、appFlavor だけで対応できます。
参考: Use flavors in Flutter code
// Flavor: 環境の種類
enum Flavor {
dev,
stg,
prod;
static Flavor get current {
final f = appFlavor ?? (throw UnimplementedError());
return Flavor.values.byName(f);
}
// tree-shaking 用: const で宣言
static const bool isDev = appFlavor == 'dev';
static const bool isStg = appFlavor == 'stg';
static const bool isProd = appFlavor == 'prod';
}
// AppEnvironment: 環境別の設定を一箇所に集約
class AppEnvironment {
static String get apiBaseUrl => switch (Flavor.current) {
Flavor.dev => 'https://example.dev.com',
Flavor.stg => 'https://example.stg.com',
Flavor.prod => 'https://example.prod.com',
};
}
// prod バイナリからこの Widget ごと除去される
if (Flavor.isDev) ...[
const DebugOverlay(),
],
# ビルドコマンドはシンプル
flutter run --flavor dev
用途に応じて const bool と switch を使い分けるのがポイントです。
| 用途 | 書き方 | const 性 | tree-shaking |
|---|---|---|---|
| フラグ(dev専用コードの除去) | const bool isDev = appFlavor == 'dev' |
✅ | ✅ |
| 値の切り替え(APIのURLなど) | switch (appFlavor) { ... } |
❌ | 不要 |
appFlavor と tree-shaking:なぜ == 比較なら動くのか
前述の通り appFlavor は const String?(nullable)ですが、== による比較は null を外す操作ではないため、const 式として有効です。
// ✅ == 比較は unwrap 不要 → const 式として有効
const bool isDev = appFlavor == 'dev';
一方、appFlavor を unwrap する操作(!、??、final への代入など)は const 式として認められず、const 性が壊れます。
// ❌ ! は const 式で使えない → const 性が壊れる
if (appFlavor! == 'dev') { devOnlyCode(); }
// ❌ getter + ?? + byName() → 完全にランタイム評価
static Flavor get current {
final f = appFlavor ?? (throw UnimplementedError());
return Flavor.values.byName(f);
}
つまり、appFlavor で tree-shaking を実現するには unwrap せず == で直接比較して const bool に入れるのがポイントです。
では --dart-define(-from-file) はいつ使う?
tree-shaking 目的であれば --flavor 単独で対応できるため、--dart-define(-from-file) を組み合わせる必要はありません。
--dart-define(-from-file) が有効なのは、flavor とは無関係なコンパイル時定数を Dart コードに注入したい場合です。例えば CI のビルド番号や、A/B テスト用のフラグなど、環境(dev/stg/prod)とは直交する値を渡す用途に適しています。
参考: Configuring apps with compilation environment declarations
まとめ
調べた結果、自分が思う用途と手段の対応をまとめます。
| 用途 | 推奨手段 |
|---|---|
| ネイティブの環境分け(アプリID・アプリ名・Firebase設定ファイルなど) | --flavor |
| Dartコードの環境分け(APIのベースURLなど) | --flavor(switch (appFlavor) で切り替え) |
| dev専用コードのtree-shaking | --flavor(const bool isDev = appFlavor == 'dev') |
| flavor と無関係なコンパイル時定数の注入 | --dart-define(-from-file) |
| 秘匿情報 | サーバーサイド管理が原則。やむを得ずクライアントに持たせる場合は envied |
個人的な推奨は --flavor 単独 です。ネイティブの環境分け・Dart側の設定切り替え・tree-shaking のすべてをこれ一つで対応できます。
おまけ
個人開発やプロジェクト新規作成時は flutter_flavorizr を使うと、flavorのセットアップをコマンド一発で済ませられておすすめです。
dart run flutter_flavorizr







