Flutterアプリにおける環境ごとの設定管理方法に関する考察

【2025/03/02追記】やはり、ビルドターゲットの方が良いという結論に考えが変わったため、[こちら](https://qiita.com/pikanji/items/d1c1836bd3a106558010)に再検討した記事を書きました。

Flutteアプリ開発において、環境ごとの設定（APIサーバのURLなど）をソースコードのどこに記述するべきかについて、調べたのでまとめる。公式ドキュメントではFlavorやビルドタイプやビルドターゲットで実現することが言及されている一方、巷ではやたら.envを使っていたりと、Flutterには環境に応じた設定を適用するスタンダードな仕組みが明確ではないが、下記が候補として考えられる。
1. [Flavor](https://docs.flutter.dev/deployment/flavors)
2. ビルドタイプ
3. ビルドターゲット
4. .envや.jsonファイル

何がベストなのか自分なりに調べて考えてみた。

# 自分なりの結論
環境毎の設定は２つの方法のどちらかで、どちらを選択するかは好みの問題。1) main_<環境名>.dartにまとめて記述して、ビルドターゲット (dart entrypoint) としてビルド設定で指定する、Flutter公式ドキュメントで言及している方法。2) .envや.jsonなど設定ファイルとして切り出して、ビルド設定で指定する。
その他、Application IDなどはFlavorとプラットフォームごとの設定で切り替え、これもビルド設定で指定して保存しておく。

* Flavor: 使い方次第ではあるが、特にapplication IDなど、ビルドに必要なプラットフォーム固有の設定情報のバリエーションを実現できるのがFlavorになるので、その用途に使用する。
* ビルドタイプ：よくあるreleaseとdebugのビルドタイプに限定して利用する。
* ビルドターゲット: 環境ごとに異なるがプラットフォームで共通な設定は、main_<variant>.dartに記述してビルドターゲット（dart entrypoint）で切り替える。
* .env: ビルドターゲットとほぼ同じことができ、Flutter公式ではないがわかりやすいので好み次第でビルドターゲットの代わりに使えばよい


# 詳細

Application IDやバンドルIDなどは、プラットフォーム毎の設定ファイルに記述するしか無く、その切替はFlavorやビルドタイプによって実現する。そういった切り替えをしたければ、プラットフォーム毎の設定とFlavorやビルドタイプを使うしかない。`--dart-define-from-file`オプションにより、ビルド時に環境別の設定を参照することができたが、Flutterバージョン3.19.0からはビルド時には参照できないように意図的に修正されている。[参考](https://github.com/flutter/flutter/pull/136865)。ただし、自前gradleスクリプトやシェルスクリプトをビルドから読み込ませてビルド時に環境変数が使えるようにすることはかのうで、[こちら](https://zenn.dev/altiveinc/articles/separating-environments-in-flutter)の記事ではそれを丁寧に紹介してくれている。しかし、個人的には、フレームワークを使っているのにわざわざこのようなボイラープレートを自分で作成するのはあまり納得感がないし、Flutterからあえて削除された機能を自分で再実装するのもなにかすっきりしない。なので今回はあくまで、公式な方法に近いものを検討してみる。

ではAPIサーバのURLの切り替えはもプラットフォーム毎の設定ファイルに書けばよいか？書けなくもないし、プラットフォーム個々に見れば、そこに書くのが正解だったりする。Androidだとsrc/developmentなどとビルドタイプやフレーバーでディレクトリを分ければそれぞれで使用するファイルを切り替えさせることができるし([参考](https://developer.android.com/build/build-variants#sourceset-build))、build.gradleファイルでFlavorごとに設定やリソースを記述することもできる([参考](https://developer.android.com/build/gradle-tips#share-custom-fields-and-resource-values-with-your-app-code))。
しかし、クロスプラットフォームのFlutterでプラットフォーム個々に記述していてはFlutterの意味が薄れてしまうので、プラットフォームのビルドに依存せずにFlutterのビルド時に共通で切り替えさせたい。

Flavorは実行時に参照できるので、Flavorを参照してロジックで設定を切り替えることも可能だが、できれば、テストや開発の情報が本番リリース用のビルドにも含まれてしまうことは避けたいので、実行時に切り替えることもしたくない。

ビルドタイプはAndroidにおいてはFlavorの１ディメンションと考えられなくもないのでFlavorとの違いが分かりづらいが、よくあるreleaseとdebugのビルドタイプに限定して利用するのがわかりやすくて良いと考える。本番向けのコードでもアップストアに登録する用のreleaseビルドと、本番でのみ発生する不具合の調査などに使用するデバッグ情報を出力するdebugビルドを作成したりする。様々なセキュリティ機能がそれを前提に作られてたりするとも考えられるので、releaseとdebug程度に限定して使うのが良いと思われる。

となると、やはり、Flavorやビルドタイプ以外に、ビルドターゲットや.envで設定を切り替える方が好ましい。


では、ビルドターゲットと.envはどう違いがあるのか、それぞれのメリット・デメリットを挙げてみる。

## ビルドターゲットと.envの比較
両者とも、任意のファイル名で保存して、ビルドオプションで指定するという点では同じだ。
要は書き方が違うだけなのだが、.envに関しては理由も知らずに「コミットするものではない」とか「クレデンシャルを書く場所」とか勘違いされるのが怖い。

### ビルドターゲット
メリット
* 公式で言及されている安心感
* 設定項目を選択的に必須にすることができ、設定漏れミスを防ぎやすい

デメリット
* .dartファイルなので設定があるファイルが見つけにくい
* ロジックと同じファイルで分かりづらい
* 設定がロジックと混ざってカオスになりかねない

### .env
メリット
* 設定がロジックから切り離されている、.dartファイルではないのでわかりやすい。

デメリット
* 非公式。3.7.0以降`--dart-define-from-file`オプションで指定できる様になったが、ドキュメントには書かれていない。また、3.16.0まではbuild.gradleなどから参照できたが現在はできなくしてあるなど、挙動が不明確。
* 設定項目が足りていなくても気づきにくい
* .envにクレデンシャルを書けば安全とか、コミットすべきではない、という勘違いをする人がいる。[詳しくはこちら](https://qiita.com/pikanji/items/6c5854679ae0fce5d558)に書きました。


## それぞれの使い方
### ビルドターゲット
実装方法は様々だがConfigクラスで設定項目を定義して、Configオブジェクトをシングルトンで持っておく。
main_dev.dart
```dart
import 'package:flutter/material.dart';
import 'package:myapp/my_app.dart';
import 'package:myapp/utilities/config.dart';

void main() async {
  Config.init(
    backendUrl: "http://localhost:3000",
    someId: "xxxxxxxxxxxxxxxxxxxxx",
  );

  runApp(MyApp());
}
```

config.dart
```dart
class Config {
  static Config _instance;
  final String backendUrl;
  final String someId;

  factory Config.init({
    @required String backendUrl,
    @required String someId,
  }) {
    if (_instance == null) {
      _instance = Config._internal(backendUrl, someId);
    }
    return _instance;
  }

  Config._internal(this.backendUrl, this.someId);

  factory Config() {
    if (_instance == null) {
      throw BadImplementationException(
          "Config.init({...}) must be called once before use of Config class.");
    }
    return _instance;
  }
}
```

ビルド時に -t オプションで main_dev.dart をターゲットとして指定するか、AndroidStudioの「Run/Debug Configration」のdart entrypointでmain_dev.dartを指定した設定を作成することで、dev用のビルドができる。
VSCodeのlaunch.jsonでは`configurations`の`program`でこのファイルを指定する。


設定を使うときは`Config().backendUrl`と参照するだけ。



### .env
.envはシンプル。
```shell
BACKEND_URL=http://localhost:3000
SOME_ID=xxxxxxxxxxxxxxxxxxxxx
```
ファイル名は何でも良いので、環境ごとに`.env.<環境名>`のようなファイルを作成してコミットしておけば環境毎の設定がわからなくなることはない。開発者ごとのローカルの設定はコミットすべきではないが。

そして、ビルド時に`--dart-define-from-file=.env`などとファイルを指定すれば良い。
AndroidStudioでは「Run/Debug Configration」の`Additional run args`で指定する。
VSCodeのlaunch.jsonでは`configurations`の`args`にこのオプションを指定すれば良い。
flutter_dotenvというパッケージを利用するのも手だ。

使用するのも簡単。値の方に応じて、下記のようにアクセスできる。
```dart
String.fromEnvironment(name)
int.fromEnvironment(name)
bool.fromEnvironment(name)
```

.envはシンプルな分、人為的なミスが発生しやすい。トレードオフだ。

# 結論
* 公式な方法を好む場合: ビルドターゲットを利用し、設定を .dart ファイルに記述する方法
* シンプルさを重視する場合: .env ファイルを使う方法

いずれの方法も、プロジェクトの要件やチームの好みに応じて柔軟に選択すれば良い。