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"

--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名を参照できます。APIのベースURLなどの切り替えも appFlavor を使ったシンプルな実装で対応できるため、--dart-defineflutter_dotenv を追加しなくても事足ります。

参考: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);
  }
}

// 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',
  };
}
# ビルドコマンドはシンプル
flutter run --flavor dev

--flavor--dart-define(-from-file) を組み合わせる理由を考えてみた

--flavor 単体でも環境分けは十分に機能します。しかし 「dev専用のコードを prod バイナリから完全に除去したい」 場合は、--flavor だけでは対応できません。これが --dart-define(-from-file)--flavor と組み合わせて使う主な理由だと思います。

appFlavor は実行時に評価されるため、コンパイラがビルド時に条件の真偽を判断できず、dead code を除去(tree-shaking)できません。一方 --dart-define で渡した値を const として使うと、コンパイラがビルド時に条件を確定し、不要なコードをバイナリから取り除けます。

dev専用の大きなコードブロックを prod バイナリから除去したい場合、ポイントは const でビルド時に値を確定させること です。

// ❌ tree-shaking 効かない(final は実行時評価)
final flavor = String.fromEnvironment('FLAVOR');
if (flavor == 'dev') { devOnlyCode(); }

// ✅ tree-shaking 有効(const はコンパイル時定数)
const flavor = String.fromEnvironment('FLAVOR');
if (flavor == 'dev') { devOnlyCode(); }

getter 経由では const 性が失われますが、@pragma でインライン展開すれば解決できます。

// ❌ getter 経由では const 性が呼び出し元に見えない
bool get isDevMode => const String.fromEnvironment('FLAVOR') == 'dev';

// ✅ @pragma でインライン展開すれば const 性が届く
@pragma('vm:prefer-inline')
bool get isDevMode => const String.fromEnvironment('FLAVOR') == 'dev';

appFlavor は実行時の値のため、@pragma を使っても tree-shaking は効きません。

実装例:

const bool isDevMode = String.fromEnvironment('FLAVOR') == 'dev';

// prod バイナリからこの Widget ごと除去される
if (isDevMode) ...[
  const DebugOverlay(),
],
flutter run --flavor dev --dart-define=FLAVOR=dev

環境変数の切り替え程度であれば tree-shaking は不要ですが、dev 専用の重いコードを prod バイナリから除去したい場面で活用できます。

まとめ

調べた結果、自分が思う用途と手段の対応をまとめます。

用途 推奨手段
ネイティブの環境分け(アプリID・アプリ名・Firebase設定ファイルなど) --flavor
Dartコードの環境分け(APIのベースURLなど) --flavorappFlavor 経由)or --dart-define(-from-file)
dev専用コードのtree-shaking --dart-define(-from-file)
秘匿情報 サーバーサイド管理が原則。やむを得ずクライアントに持たせる場合は envied

個人的な推奨は以下の2パターンです。

  • --flavor 単独 — シンプルで十分なケースはほとんどこれで対応できます。
  • --flavor + --dart-define-from-file — dev専用のコードをprodバイナリから完全に除去したい場合(tree-shaking)に採用します。--dart-define ではなく --dart-define-from-file を使うのは、設定値が複数あるときに1ファイルで管理でき、ビルドコマンドもスッキリするためです。

おまけ

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

dart run flutter_flavorizr

この記事をシェアする

FacebookHatena blogX

関連記事