Flutter 環境分けの手法選定を考察してみた ~ --dart-define(-from-file) でネイティブ連携しない理由を添えて ~

Flutter 環境分けの手法選定を考察してみた ~ --dart-define(-from-file) でネイティブ連携しない理由を添えて ~

Flutterプロジェクトの環境分けに使われる ‎`--flavor` / ‎`--dart-define` / ‎`--dart-define-from-file` / ‎`flutter_dotenv` / ‎`envied` の役割と選定基準を整理し、どの手法をどの用途で使うべきかを考察しました。
2026.03.29

概要

Flutterプロジェクトの環境分けの実装は様々です。

  • --flavor のみで環境分けしているプロジェクト
  • --flavor + --dart-define または --dart-define-from-file を組み合わせているプロジェクト
  • flutter_dotenvenvied を使っているプロジェクト

それぞれの手法について、選定基準を言語化できるほど自分が理解できていなかったため、改めて調べてみました。

なお、この記事では各手法の選定基準と役割の整理を目的としており、各環境分けのセットアップ手順・実装手順は扱いません。

伝えたいこと

  • 各手法(--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-fileflutter_dotenvenvied と、環境分けを実現する手段が複数存在します。それぞれ異なる仕組みで動作するため、どれを使うべきか判断しにくいです。

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

appFlavorpackages/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 チームが「意図しない副作用」と判断しバグ修正として削除されています。この経緯を知らないと、「なぜネイティブから読めないのか」「なぜハックな実装をしているプロジェクトがあるのか」が理解しにくくなります。

それでも値は 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 boolswitch を使い分けるのがポイントです。

用途 書き方 const 性 tree-shaking
フラグ(dev専用コードの除去) const bool isDev = appFlavor == 'dev'
値の切り替え(APIのURLなど) switch (appFlavor) { ... } 不要

appFlavor と tree-shaking:なぜ == 比較なら動くのか

前述の通り appFlavorconst 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など) --flavorswitch (appFlavor) で切り替え)
dev専用コードのtree-shaking --flavorconst bool isDev = appFlavor == 'dev'
flavor と無関係なコンパイル時定数の注入 --dart-define(-from-file)
秘匿情報 サーバーサイド管理が原則。やむを得ずクライアントに持たせる場合は envied

個人的な推奨は --flavor 単独 です。ネイティブの環境分け・Dart側の設定切り替え・tree-shaking のすべてをこれ一つで対応できます。

おまけ

個人開発やプロジェクト新規作成時は flutter_flavorizr を使うと、flavorのセットアップをコマンド一発で済ませられておすすめです。

dart run flutter_flavorizr

この記事をシェアする

関連記事