Flutterで本番/開発 別に切り分けてアプリを入れる

## 概要
Flutterでアプリ開発をしていると、本番アプリ、開発アプリの名前を分けて確認したいことありますよね。
この切替には複数のSchemeを用意する方法、Flavorを使う方法、ビルド時の--releaseと--debugを使って切り分ける方法がありますが、Flutter 1.17からビルド引数の--dart-defineで定数を渡せるようになりました。今回はこの`--dart-define`の方法で本番、開発それぞれのアプリをスマホ内に入れる方法を説明します。
![Qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/e55a133d-37c2-4246-8a97-9293a56adcd1.png)

### 前提
* Flutterのサンプルプロジェクトを作成しシミュレーターで実行できる
* Flutter 1.20以降
* --dart-define の設定ができる（[こちらの記事](https://qiita.com/yana1316/items/542dd4f787779d154f45)を参照）

### 環境
```
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[+ ] Flutter (Channel stable, 1.22.4, on Mac OS X 10.15.7 19H2 darwin-x64, locale en-JP)
[+ ] Android toolchain - develop for Android devices (Android SDK version 30.0.0)
[+ ] Xcode - develop for iOS and macOS (Xcode 12.1)
[+ ] Android Studio (version 4.1)
[+ ] Connected device (1 available)
```     

## iOS用の設定
iOSの場合`Bundle identifer`でアプリを識別しています。この値が同じものだと上書きインストールされてしますので、この値を本番と開発で変える必要があります。

### Flutterの設定
以下のように、コンパイル引数で渡します。
開発用
`flutter build ios --dart-define=DEFINE_IOS_SUFFIX=.dev`
本番用
`flutter build ios --dart-define=DEFINE_IOS_SUFFIX=`
※ --dart-define の基本的な設定については[こちらの記事](https://qiita.com/yana1316/items/542dd4f787779d154f45)を見てください。（記事中の`Info`>`Custom iOS Target Properties`は設定せず、下記'Xcodeの設定'を参考に設定してください）

### Xcodeの設定
`Build Settings`の`Packaging`にある`Product Bundle Identifier Debug`の値をダブルクリックし、もともとの値の後ろに`$(DEFINE_IOS_SUFFIX)`を追加します。
![Runner_xcodeproj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/67b9d84e-7f96-5533-9a26-db1164236e62.png)

`Runner/Info.plist`の`Information Property List`にある`Bundle name`にあるもともとの値の後ろに`$(DEFINE_IOS_SUFFIX)`を追加します。
![Info_plist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/a7d1bcd7-8cf7-9498-b6f2-07601e2dc0ff.png)


### 動作確認
開発用、本番用、それぞれ実行してみてください。
どうでしょうか？スクリーンに`DEFINE_IOS_SUFFIX`のついた`Bundle Identifer`が表示されたでしょうか。また、ホームスクリーンにも2つのアプリが入ったと思います。
![iPhone_SE_–_2nd_generation_–_14_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/95288ca8-91f0-d2d1-9a23-4fa0b6cbd439.png)
※ 元の名前が長くて「flutter_define...」となってしまいました。
![Qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/e55a133d-37c2-4246-8a97-9293a56adcd1.png)

## Android用の設定
Androidの場合は`applicationId`によってアプリが識別されます。この値を本番と開発とで変えていきます。

### Flutterの設定
以下のように、コンパイル引数で渡します。
開発用
`flutter build apk --dart-define=DEFINE_APP_NAME=flutter_define_dev --dart-define=DEFINE_APP_SUFFIX=dev`
本番用
`flutter build apk`

### Gradleの設定
`[project]/android/app/src/main/AndroidManifest.xml`にある`android:label`を修正します。

```xml
<application
    android:name="io.flutter.app.FlutterApplication"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher">
```

`[project]/android/app/build.gradle`を以下のように修正します。

```gradle
// 〜〜 省略 〜〜
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

// ここから追記 --
def dartEnvironmentVariables = [
        DEFINE_APP_NAME: 'flutter_define',
        DEFINE_APP_SUFFIX: null,
];
if (project.hasProperty('dart-defines')) {
    dartEnvironmentVariables = dartEnvironmentVariables + project.property('dart-defines')
            .split(',')
            .collectEntries { entry ->
                def pair = URLDecoder.decode(entry).split('=')
                [(pair.first()): pair.last()]
            }
}
// ここまで追記 --

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

// 〜〜 省略 〜〜

android {

    defaultConfig {
        applicationId "com.yana1316.flutter_define"
        applicationIdSuffix dartEnvironmentVariables.DEFINE_APP_SUFFIX // 追記
        minSdkVersion 16
        targetSdkVersion 29
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        resValue "string", "app_name", dartEnvironmentVariables.DEFINE_APP_NAME // 追記
    }
```

### 動作確認
開発用、本番用、それぞれ実行してみてください。
どうですか？スクリーンに`applicationId`が表示されたかと思います。また、アプリも2つ入りました。
![Android_Emulator_-_Nexus_S_v9_0_5554.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/e1467127-10f8-6145-6dcf-f0913d9feaa7.png)

![Android_Emulator_-_Nexus_S_v9_0_5554.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/04eebc8e-9048-012b-d73f-4f786639aff6.png)

## まとめ
以前はFlavorや複数のShceme、`--target`オプションや`--rerease`と`--debug`オプションなどを使って切り替えていました。Flutter1.17から導入された`--dart-define`を使うとシンプルに切り分けができたかと思います。
組織で開発をするときはBitriseやfastlaneが既に導入され、自動的に便利に開発できることも多いと思います。一方、個人開発や初めてFlutterを導入する場合にて、この程度の切り分けがちょうどよいケースもありますので、参考になればと思いました。

## 参考
- [Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything](https://itnext.io/flutter-1-17-no-more-flavors-no-more-ios-schemas-command-argument-that-solves-everything-8b145ed4285d)
- [Flutter 1.20 — What you should know before you upgrade your project that uses compile-time variables](https://medium.com/@tatsu.ukraine/what-you-should-know-before-you-upgrade-flutter-in-your-project-with-compile-time-variables-3ec3d2e9ba79)