GitHub Actions利用を見据えたFlutterアプリの環境別ビルド( Flutter 3.19以降での dart-define-from-file 利用 )

## はじめに
株式会社 ONE COMPATH でアプリ開発を担当しているハラです。

近年は当社のスマホアプリでもFlutter製のものが増えてきており、GitHub Actionsを用いたCI/CDの導入も進んでおります。

本記事では、GitHub Actions 利用を想定したFlutterアプリの環境別ビルドを、現在開発を進めている当社のアプリでどのように構築しているか紹介いたします。

※ 本記事では GitHub Actions での具体的な設定については紹介いたしません。

## 前提
- Flutter 3.24.2
- Dart 3.5.2
- ビルド対象はスマホアプリ(iOS/Android)のみ

## 出し分けの項目
ここでいう「環境出し分け」は、例えば「開発環境用」「STG環境用」「本番リリース用」などのビルドの目的に応じて特定の設定値を切り替えることを指しています。

以下のようなものを出し分ける想定で、記事を進めます。

- AppID(Bundle ID)
- アプリ名(端末ホーム画面アイコンの下部に表示される名称)
- カスタムURLスキーム
- iOSのプロビジョニングプロファイル
- サービス独自のAPIのホスト
- サービス独自のAPIを利用するためのAPIキー
- GoogleMapのAPIキー
- サードパーティサービスのAPIキー
- アプリ内デバッグ機能を有効化するか否か
- Firebase設定(本記事では詳細割愛)

## 環境別の出し分け方法
このアプリを実装し始めた当初、Flutterのバージョンが3.10くらいだったので `dart-define-from-file` を利用した環境出し分けを入れました。
ところが `3.19` 以降にそのままではAndroid/iOSのネイティブコードからの利用できなくなったので、 以下の記事などを参考に、ネイティブからも設定値を呼べるように対応しています。

https://zenn.dev/altiveinc/articles/separating-environments-in-flutter

https://techblog.reazon.jp/entry/2024/05/09/142401

初期の設定はやや面倒ですが、
- 設定変更時にネイティブそれぞれのファイルを見に行かなくて良い
- 複数の設定項目を1つのファイルで管理できる

といったメリットがあり、引き続きネイティブ部分へ値を渡すためにも利用しています。

ここで注意しておきたいのは、上で列挙した出し分けたい項目のうち、APIキーなどはGitHubにpushするべきではありません。
しかしアプリ名やカスタムスキーム追加など、Gitで履歴管理したい項目もあります。

そこで、本アプリでは、`dart-define-from-file` で指定するファイルを2種類用意することにしました。

 `dart_defines` ディレクトリを作り、その中に以下のように2種類の設定ファイルを環境別に用意しています。

```
settings_dev.json
settings_stg.json
settings_prd.json

secret_dev.json
secret_stg.json
secret_prd.json
```

このうち、`settings_***`のほうには
- AppID
- アプリ名
- カスタムURLスキーム
- プロビジョニングプロファイル名(名称のテキストのみ)
- APIのホスト名
- デバッグ機能有効フラグ

等を設定してGitのコミットに含め、

`secret_***` のほうには
- APIキー
- アクセストークン
- Firebase設定(これはsettings側でも良いかもしれない)

等を設定、 `.gitignore` の対象にしてGitコミットには含まれないようにし、別途、社内ローカルの共有サーバ等にファイルを置いて管理、

という扱いとしています。

具体的には例えばこんな感じ。

``` dart_defines/settings_dev.json (stg/prdなども同様に)
{
    "app_id": "com.example.app.hogehoge.dev",
    "app_name": "[DEV]マイアプリ",
    "custom_url_scheme": "my-scheme-dev",
    "provisioning_profile_name": "Myサンプルアプリ用(DEV)",
    "my_api_host": "api-dev.example.com",
    // ...
}
```

``` dart_defines/secret_dev.json (stg/prdなども同様に)
{
    "my_api_key": "xxxxxxxxxxx",
    "google_maps_key": "XXXXXXXXXXXXXX",
    "hoge_service_key": "xxxxxx",
    // ...
}
```

### ローカルでのビルド
実行/ビルド時は
```
flutter run --dart-defines-from-file=dart_defines/settings_dev.json --dart-defines-from-file=dart_defines/secret_dev.json
```
のように、`--dart-defines-from-file` をファイル数の分渡してあげれば大丈夫です。

なお、VSCode を利用して開発を行なっているため、.vscode/launch.json の configuration内に以下を追加し、ワンクリックでdev環境での実行をできるよう設定しています。
``` .vscode/launch.json の一部
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug dev",
            "request": "launch",
            "type": "dart",
            "cwd": "app_main",
            "flutterMode": "debug",
            "args": [
                "--dart-define-from-file=dart_defines/settings_dev.json",
                "--dart-define-from-file=dart_defines/secret_dev.json"
            ]
        },
        //以下略....
    ]
}
```

### GitHub Actions でのビルド

GitHub Actions での実行時には、secret_***.json ファイルの中身をBase64エンコードした値を、 `Actions secrets and variables` の変数に登録して利用しています。

```
base64 -i dart_defines/secret_stg.json | pbcopy
```
これを例えば `DART_DEFINES_SECRET_STG_JSON_BASE64` として登録しておきます。

![add_secret.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684189/5013232b-4216-1fe5-40b6-4b8f2822d4f7.png)



そして、Actionsのジョブ内で、ビルド前に
```
echo ${{ secrets.DART_DEFINES_SECRET_STG_JSON_BASE64 }} |  base64 --decode > dart_defines/secret_stg.json
```
のように復元します。
そしてその後のビルドコマンドにて `--dart-define-from-file` で復元したファイルを指定しています。

## 設定値の参照方法
先述のリンク先からの引用も含まれていますが、各所からの利用方法は以下の通りです。

### Flutter/Dartコードからの利用
`--dart-define-from-file` で渡したファイル内のkey-valueペアは、個別に `--dart-defines` で渡したものとして扱われます。
すなわちFlutter(Dart)から見ると環境変数として反映されるので
```
const myApiKey = String.fromEnvironment("my_api_key");
```
のように取得できます。

### ネイティブからの利用設定
先述の通り、 `--dart-define-from-file` で渡したファイル内の値は、個別に `--dart-defines` で渡したものとして扱われますが、Flutter 3.19 以降では、これをAndroid/iOSネイティブの設定ファイルやソースコード内からそのまま直接利用することはできなくなりました。
そこで、ネイティブでのビルド前に、 `dart-defines` で渡された値をネイティブから利用できるような処理を追加することが必要になります。

#### Android


`dart-defines` で受け取った値を `app/build.gradle` 内で処理して変数として格納し、それを適宜リソースファイル・manifestPlaceholders・`BuildConfig` などに渡して各所で利用します。

``` android/app/build.gradle

// ...関係ない箇所は省略

// `dart-defines`で渡された値を、変数リストに格納
// これにより、このbuild.gradle 内では、
// dartEnvironmentVariables.xxxxxx
// のように値を利用できる
def dartEnvironmentVariables = [];
if (project.hasProperty('dart-defines')) { dartEnvironmentVariables = project.property('dart-defines')
        .split(',')
        .collectEntries { entry ->
            def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
            [(pair.first()): pair.last()]
        }
}
// ...略


android {
   // ...略
   // applicationIdに、dart-define-from-file で定義した値を設定する例
   applicationId dartEnvironmentVariables.app_id
   // リソース(R.string.xxxx) に値を設定する方法
   resValue "string", "app_name", dartEnvironmentVariables.appName
   // ↑これでAndroidManifestのapplication内でに
   //   `android:label="@string/app_name"`
   // とすればアプリ名が反映される

   // AndroidManifestから利用する方法例
   // 上記のR.string.xxx に指定する方法でも良いが、それでうまくいかないところではこれ
   // これにより
   //   `<data android:scheme="${customScheme}" />`
   // のような使い方で、設定値をAndroidManifest内から利用できる
   manifestPlaceholders += [
        "googleMapsApiKey": dartEnvironmentVariables.google_maps_key,
        "customScheme": dartEnvironmentVariables.custom_url_scheme,
        // ...
   ]

   // Andoridのネイティブコード(Kotlin/Java)から利用する方法
   // 先述の R.string.xxxx に反映させる方法でも良いですが、こちらはBuildConfigに反映する方法
   // これで、Java/Kotlinから BuildConfig.HOGE_SERVICE_KEY として取得できます
   buildConfigField "String", "HOGE_SERVICE_KEY", "\"${dartEnvironmentVariables.hoge_service_key}\""

   // ...略
}
    

```

#### iOS
Xcodeを開いて、Edit Scheme.. でシェルスクリプトを追記し、
dart-defines の各項目をXcode内の変数として利用できるようにします。

具体的な方法は、以下の参照記事の iOS の項目そのままなので、本記事では割愛します。

https://techblog.reazon.jp/entry/2024/05/09/142401#iOS

設定値の利用方法ですが、Xcode上での Info.plistやBuild Settings内などでは
`$(app_id)`
のように使えるので、適宜必要な箇所に入れていけばOKです。

ただし Provisioning Profile の名称は、 Xcode上ではテキストで指定できなかったので、
`ios/Runner.xcodeproj/project.pbxproj` をエディタで開いて、 `PROVISIONING_PROFILE_SPECIFIER` の行を直接以下に書き換えました。
```
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "${provisioning_profile_name}";
```

また、Swiftのコードから値を利用したい場合は、
info.plistに値を設定して呼び出せばOKです。

``` Info.plist の一部
<dict>
...略...
	<key>HOGE_SERVICE_KEY</key>
	<string>$(hoge_service_key)</string>
...略...
</dict>
```

``` AppDelegate.swift (swiftからの利用例)
let hogeServiceKey = 
    Bundle.main.object(forInfoDictionaryKey: "HOGE_SERVICE_KEY") as? String
```

## おわりに

本記事では、dart-define-from-file を利用してFlutterアプリの環境別ビルドを行う方法について紹介しました。
GitHub Actions での具体的な設定についてはほとんど紹介しませんでしたが、近々そのあたりの記事も書けたらと思っております。