Flutterで新規アプリ作る時にやることまとめ

Flutterで新規にアプリを作る時にテンプレ的に大体同じような事をやっているが、いつも忘れるので備忘録としてまとめ。

Flutterは絶賛開発中なので、本稿における記載は当時はこれでうまくいっていたぐらいに留めていただけると幸いです。
また、本稿は個人的なメモの意味合いが強いため、割愛している説明が多々あります。

**Flutter2での動作確認と全編リファクタした新バージョンは[こちら](https://zenn.dev/kingu/articles/9192b91062841b8e0bba)**

# 環境
`flutter --version`

```
Flutter 1.22.0 • channel stable • https://github.com/flutter/flutter.git
Framework • revision d408d302e2 (5 weeks ago) • 2020-09-29 11:49:17 -0700
Engine • revision 5babba6c4d
Tools • Dart 2.10.0
```

# Flutter Create

プロジェクトを作る。

```bash
flutter create --org com.example your_project_name
```

## メモ
`flutter create -h`に書いてあるとおり、iOSとAndroidネイティブ実装側は`Swift`と`Kotlin`がデフォルトで指定されるので、もし変更したい場合は`--ios-language`や`--android-language`で指定すること。

orgはパッケージ名に影響するため、後から変更するのは面倒。
特に`android/app/src/main/kotlin/**`のディレクトリにも影響するため、掃除が面倒なので後から変更するなら注意。

# Firebase プロジェクト作る

Cloud FirestoreやAuthenticationを使わないからFirebase必要ない場合もあると思うが、[Crashlytics](https://firebase.google.com/products/crashlytics)や[Performance](https://firebase.google.com/products/performance)はとても便利なので裏方としてだけでも導入するのはオススメ。

- https://console.firebase.google.com/

devやprodなど必要な環境分だけ作っておこう。
`アプリを実行してインストールを確認`とかいう疎通確認のステップはとりあえず無視。

## デフォルトの GCP リソース ロケーション 

大抵の場合は東京である`asia-northeast1`で良いとは思う。

## App Distribution

App Distributionを利用するつもりなら、コンソールからiOS/Android各アプリ毎に`開始`をクリックしておくことを忘れずに。
予め開始しておかないとアップロードする際にエラーが出る

```
Error: App Distribution could not find your app 1:hoge:ios:hoge. Make sure to onboard your app by pressing the "Get started" button on the App Distribution page in the Firebase console: https://console.firebase.google.com/project/_/appdistribution
```

## 構成ファイル

構成ファイルはブラウザからポチッとダウンロードしてもいいが、[firebaseコマンド](https://firebase.google.com/docs/cli?hl=ja)でダウンロードもできるので、[スクリプト](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/scripts/downloadFirebaseAppConfiguration.sh)を書いておくと楽。
（このスクリプトでは後述するFlavor対応のために適切なディレクトリとファイル名で配置するようにしている。）

# Androidの署名

keystoreはGUIから生成してもいいし、[android \- How can I create a keystore? \- Stack Overflow](https://stackoverflow.com/a/15330139/12856415)を参考にして生成してもよい

```sh:例えば
keytool -genkey -v -keystore release.keystore -alias key0 -keyalg RSA -keysize 2048 -validity 10000
keytool -importkeystore -srckeystore release.keystore -destkeystore release.keystore -deststoretype pkcs12
```
`android/app/build.gradle`で指定することになるので、`android/app/release.keystore`に置くとよい。

SHAは[Authenticating Your Client  \|  Android 用 Google API  \|  Google Developers](https://developers.google.com/android/guides/client-auth)などを参考にして確認して、Firebaseに登録しておく。

```sh:例えば
keytool -list -v -alias key0 --keystore release.keystore
```

## build.gradle

`signingConfigs`にkeystoreを追記し、releaseビルド時に利用する。
keystoreのパスワードは必要に応じて隠そう。
参考: [今更ながらAndroid の keystore と 署名\(signingConfigs\) の管理・運用について考えてみた \- Qiita](https://qiita.com/taki4227/items/c5685ec722a195764237)

```android/app/build.gradle
    signingConfigs {
        release {
            storeFile file("release.keystore")
            storePassword "android"
            keyAlias "key0"
            keyPassword "android"
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
```

# Flavor

`dart-define`を使った方法を紹介
（`--flavor`を使った場合よりも簡単？)

## Android

アプリ名の部分を`@string/app_name`に修正

```xml:android/app/src/main/AndroidManifest.xml
<application
    android:label="@string/app_name">
```

`dart-define`からの入力をパースしてアプリケーションIDのSuffixとアプリ名にセットする

```gradle:android/app/build.gradle
def dartEnvironmentVariables = [
        APP_NAME: 'デフォルト',
        APP_SUFFIX: null,
        APP_ENV: 'default'
]

if (project.hasProperty('dart-defines')) {
    dartEnvironmentVariables = dartEnvironmentVariables + project.property('dart-defines')
            .split(',')
            .collectEntries { entry ->
                def pair = URLDecoder.decode(entry).split('=')
                [(pair.first()): pair.last()]
            }
}
android {
...
    defaultConfig {
        applicationIdSuffix dartEnvironmentVariables.APP_SUFFIX
        resValue "string", "app_name", dartEnvironmentVariables.APP_NAME
    }
...
}

```

### Firebaseを利用するなら

FirebaseのSDKは`android/app/google-services.json`を自動で読み込むのでFlavorに応じて差し替えてあげる。

まず[Android Installation \| FlutterFire](https://firebase.flutter.dev/docs/installation/android)に従って`android/build.gradle`などに依存を追加していく。

次に以下のタスクを追加して、Flavorに応じた`google-services.json`を配置する。

```gradle:android/app/build.gradle
task copyGoogleServicesJson(type: Copy) {
    from "google-services-${dartEnvironmentVariables.APP_ENV}.json"
    into './'
    rename "(.+)-${dartEnvironmentVariables.APP_ENV}.json", '$1.json'
}

tasks.whenTaskAdded { task ->
    if (task.name == 'processDebugGoogleServices') {
        task.dependsOn copyGoogleServicesJson
    }
    if (task.name == 'processReleaseGoogleServices') {
        task.dependsOn copyGoogleServicesJson
    }
}
```

最後に`android/app/google-services.json`はFlavorを変える毎に変更されるのでgitignoreに追加しておく。

```sh
echo "android/app/google-services.json" >> .gitignore
```



## iOS

Info.plistに`APP_NAME`をセット。
`PRODUCT_NAME`にしておくとexportする際に`Runner.ipa`で出力される

```diff:ios/Runner/Info.plist
        <key>CFBundleName</key>
-       <string>アプリの名前</string>
+       <string>$(PRODUCT_NAME)</string>
        <key>CFBundleDisplayName</key>
-       <string>$(DISPLAY_NAME)</string>
+       <string>$(APP_NAME)</string>
```

`Build Settings`から`Product Bundle Identifier`を検索して以下のように`APP_SUFFIX`をセット

```
com.example.app$(APP_SUFFIX)
```

つまり以下のような変更になる

```diff:ios/Runner.xcodeproj/project.pbxproj
- PRODUCT_BUNDLE_IDENTIFIER = com.example.app;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.example.app$(APP_SUFFIX)";
```

次にRunnerのSchemeをEditして、BuildのPre-actionsに[flutter 1\.19 parse sh script](https://gist.github.com/TatsuUkraine/99d2af08f6dfef17982299afa56f6507#file-ios_parse-sh)をコピペする。
最後の処理のxcconfigに書き出す部分は適切に修正すること。

```sh:今回の例だと
printf "%s\n" "${define_items[@]}"|grep '^APP_' >> ${SRCROOT}/Flutter/Generated.xcconfig
```

`Provide build settings from`は`Runner`を選択

### Firebaseを利用するなら

FirebaseのSDKは`ios/Runner/GoogleService-Info.plist`を自動で読み込むのでFlavorに応じて差し替えてあげる。

まず適当な内容で`GoogleService-Info.plist`を作って、Xcodeの`Project Navigator`で`Runner`ディレクトリ下にドラッグアンドドロップして`GoogleService-Info.plist`への参照を作っておく。
参考: [iOS Installation \| FlutterFire](https://firebase.flutter.dev/docs/installation/ios)

```sh
echo '
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>
' > ios/Runner/GoogleService-Info.plist
```

`Build Phases`の`New Run Script Phase`で[GoogleService\-Info\.plistをFlavor毎に切り替えるRunScript dart\-defineを使った場合](https://gist.github.com/KoheiKanagu/82d18621ca57e96a0a4e4a456383c006)をコピペ。

作ったRun Scriptの`Output Files`に以下を追加

```
$SRCROOT/Runner/GoogleService-Info.plist
```

作ったRun Scriptは`Copy Bundle Resources`よりも上に配置する。

各FlavorのGoogleService-Info.plistは以下のように配置しておく

- `ios/Runner/GoogleService-Info-dev.plist`
- `ios/Runner/GoogleService-Info-prod.plist`

最後に`ios/Runner/GoogleService-Info.plist`はFlavorを変える毎に変更されるのでgitignoreに追加しておく。

```sh
echo "ios/Runner/GoogleService-Info.plist" >> .gitignore
```

### Runner.entitlements

Flavor毎にApp GroupsやAssociated Domainsを切り替える場合、`./ios/Runner/Runner.entitlements`には次のように変数にしておき、`dart-define`で渡す

```xml:./ios/Runner/Runner.entitlements
	<key>com.apple.developer.associated-domains</key>
	<array>
		<string>${APP_ASSOCIATED_DOMAIN}</string>
	</array>
```



## Flutter

以下のようにして`dart-define`で値を渡す
（`APP_NAME="devアプリ"`のようにすると`"`まで渡されてしまうので注意）
（`APP_SUFFIX`が不要の場合は`--dart-define APP_SUFFIX=""`ではなく削除する）

```sh
flutter build ios \
--dart-define APP_NAME=devアプリ \
--dart-define APP_SUFFIX=.dev \
--dart-define APP_ENV=dev
```

Flutter側で値を使いたいならこのようにする。（finalではなくconstにすること）

```dart
  const appName = String.fromEnvironment('APP_NAME', defaultValue: 'unknownName');
  const appSuffix = String.fromEnvironment('APP_SUFFIX', defaultValue: 'unknownSuffix');
  const appEnv = String.fromEnvironment('APP_ENV', defaultValue: 'unknownEnv');
```

## AndroidStudio

所定の`Run/Debug Configurations`の`Additional arguments`に以下のように追記して値を渡す
（`APP_NAME="devアプリ"`のようにすると`"`まで渡されてしまうので注意）
（`APP_SUFFIX`が不要の場合は`--dart-define APP_SUFFIX=""`ではなく削除する）

```sh
--dart-define APP_NAME=devアプリ --dart-define APP_SUFFIX=.dev --dart-define APP_ENV=dev
```
[runConfigurationsの例](https://github.com/KoheiKanagu/my_flutter_app_template/tree/master/.idea/runConfigurations)


Configurationsもコミットする場合は`git add --force`が必要。
（`flutter create`ではデフォルトで`.idea/`がgitignoreされているので手動で追加する必要がある）

```sh
git add --force .idea/runConfigurations/*.xml
```


## 参考文献

- [Flutter 1\.17 — no more Flavors, no more iOS Schemas\. Command argument that changes everything \| by Denis Beketsky \| ITNEXT](https://itnext.io/flutter-1-17-no-more-flavors-no-more-ios-schemas-command-argument-that-solves-everything-8b145ed4285d)
- [dart\-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける iOS編 \- Qiita](https://qiita.com/tetsufe/items/3f2257ac12f812d3f2d6)
- [dart\-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける Android編 \- Qiita](https://qiita.com/tetsufe/items/29cc779592171dcfb7aa)
- [ビルド環境によるApp Groupsを変更する設定方法 \- Qiita](https://qiita.com/dolfalf/items/3db85bdae0c18985314a)

# 輸出コンプライアンス
「いいえ」と答える場合はInfo.plistにあらかじめ追記しておくと手動でいいえする必要がないので楽。

```diff:ios/Runner/Info.plist
+	<key>ITSAppUsesNonExemptEncryption</key>
+	<false/>
```

![スクリーンショット 2020-12-16 10.08.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46022/ff04f0c1-dd4e-261c-b1a6-f065bbb0dc55.png)


# FlutterFire

導入したいPluginを選ぼう！！！
https://firebase.flutter.dev/

導入方法は各PluginのREADMEが最新で正確なので割愛。

## Improve iOS Build Times

iOSのFirestore SDKはC++で50万行あるらしいので、普通に参照するとビルドに時間がかかる。
そこでプリコンパイルされたバージョンを参照すると大幅にビルド時間を短縮できるのでおすすめ。
[FlutterFire Overview \| FlutterFire](https://firebase.flutter.dev/docs/overview#improve-ios-build-times)

# コードのカバレッジ計測

コードカバレッジ計測する場合、以下のように`--coverage`を付与すれば`lcov.info`が出力される

```sh
flutter test --coverage --coverage-path=coverage/lcov.info
```

ただし、これは**テストの対象になっているコードの結果のみ**が出力されるため、テストコードを全く書いていないと`lcov.info`には何も出力されない。

そこで、全部のdartファイルをimportするテストコードを用意しておけば、`flutter test --coverage `した時に全てのコードが`lcov.info`に含まれるようになる

```test/coverage_test.dart
import 'package:example_app/hoge.dart';

void main() {}
```

手動でやるのは面倒なので[ヘルプスクリプト](https://github.com/KoheiKanagu/dart_full_coverage)で生成する

```sh
curl https://raw.githubusercontent.com/KoheiKanagu/dart_full_coverage/master/dart-coverage-helper | sh
```

gitignoreもしておく

```sh
echo "lcov.info" >> .gitignore
```

## 参考
[Flutter test coverage will not report untested files · Issue \#27997 · flutter/flutter](https://github.com/flutter/flutter/issues/27997)
[How can I generate test coverage of untested files on my flutter tests? \- Stack Overflow](https://stackoverflow.com/questions/54602840/how-can-i-generate-test-coverage-of-untested-files-on-my-flutter-tests)
[priezz/dart\_full\_coverage: Helper for full tests coverage checkup for you Dart/Flutter project](https://github.com/priezz/dart_full_coverage)

# CICDについて

Github Actionsでの例を示す。
条件として、AndroidのビルドはGithubのubuntu-latest、iOSのビルドは私物のMacでself-hostedを利用する。
(Androidもself-hostedにしたければ指定するだけで対応できる)

そのため、iOSの[証明書など](https://developer.apple.com/account/resources/identifiers/list)はself-hostedのMacでよしなに整備されているものとする。
（要はプライベートリポジトリでGithub Actions使いたいけどmacOSのランナーはLinuxに比べて[そこそこのお値段](https://docs.github.com/ja/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions)だから使ってないMacを活用しつつコストダウンという算段）

## ジョブの流れ

詳細は割愛するが、git-flowに合わせる

[pull\_requests\.yml](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/.github/workflows/pull_requests.yml)
1. プルリク作成/更新されたら開始
2. テスト実行
3. Codecov
4. Slackで通知

[releases\.yml](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/.github/workflows/releases.yml)
1. プルリクがmasterかdevelopにマージされたら開始
2. テスト実行
3. Codecov
4. バージョンのタグを貼る
5. Releaseを作成
6. release/*ブランチからmasterブランチにマージする際にはdevelopブランチに`#minor`と空コミット
7. Slackで通知
8. ビルド(devのiOS・devのAndroid・prodのiOS・prodのAndroidをパラレルに実行)
9. Release Assetに成果物（apk、aab、xcarchive、prodのipa）をアップロード
10. App Distributionで配布
11. Slackで通知

`TODO`としている部分には適切な値を入れること。
2つとも`.github/workflows/`に配置する

### bumping （releases.ymlのステップ6について）

タグを振るアクションの機能で、コミットのコメントに`#minor`などと書くとbumpしてくれるので、リリースしたタイミングでdevelopに`#minor`とコミットすることでbumpされる
[anothrNick/github\-tag\-action: A Github Action to tag a repo on merge\.](https://github.com/anothrNick/github-tag-action#bumping)

## Secrets

リポジトリのSecretsに以下をセットしておく

- [Slack API: Applications \| Slack](https://api.slack.com/apps)からアプリを作成してIncoming WebhooksのWebhook URLを`SLACK_WEBHOOK_URL`にセット
- [Firebase CLI リファレンス](https://firebase.google.com/docs/cli?hl=ja#cli-ci-systems)を参考にしてトークンを取得して`FIREBASE_TOKEN`にセット（[Firebase App Distribution](https://firebase.google.com/docs/app-distribution?hl=ja)を使わないなら不要）
- [Codecov](https://codecov.io/gh)から`CODECOV_TOKEN`を取得してセット（Codecov使わないなら不要）

## ビルドスクリプト

プロジェクトルートに`scripts`というディレクトリを作って中に以下のファイルを入れておく（shは実行権限を忘れずに`chmod +x ./scripts/*.sh`）

- [test.sh](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/scripts/test.sh)
- [buildiOS.sh](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/scripts/buildiOS.sh)
- [buildAndroid.sh](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/scripts/buildAndroid.sh)
- [AdHocExportOptions.plist](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/scripts/AdHocExportOptions.plist)
- [AppStoreExportOptions.plist](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/scripts/AppStoreExportOptions.plist)

なおbuildiOS.shとbuildAndroid.shにおいては、`APP_NAME`と`APP_SUFFIX`をよしなに変更すること。また、`APP_ASSOCIATED_DOMAIN`は必要であれば。

AdHocExportOptions.plistとAppStoreExportOptions.plistにおいては`teamID`をよしなに変更すること。

## Codecov

プロジェクトルートに[codecov\.yml](https://docs.codecov.io/docs/codecov-yaml)を配置すれば、カバレッジには含めないファイルを指定できるなど、いろいろ設定できる。
例えば https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/codecov.yml


## workflow_dispatchでビルド

何らかの理由で手動でビルドしたい場合のジョブ
[build\_app\_with\_workflow\_dispatch\.yml](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/.github/workflows/build_app_with_workflow_dispatch.yml)

成果物の出力などはせず、ただビルドするだけ

# アプリの標準言語を日本語にする

[Xcodeでアプリの標準言語を日本語にする方法 \- Qiita](https://qiita.com/ko2ic/items/8918034d940f66fee97d)

記事では`developmentRegion`は`Japanese`にとあるが`ja`にすること

```diff:ios/Runner.xcodeproj/project.pbxproj
- developmentRegion = en
+ developmentRegion = ja
```

## localizationsDelegates

`GlobalWidgetsLocalizations`などを指定するなら`pubspec.yaml`に追記しておく

```main.dart
    return MaterialApp(
      localizationsDelegates: const [
        GlobalCupertinoLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
    );
```


```pubspec.yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: any
```

# Firebase CrashlyticsのdSYM

dSYMをアップロードするスクリプト
[uploadSymbols\.sh](https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/scripts/uploadSymbols.sh)

## 参考

[可読化されたクラッシュ レポートを取得する  \|  Crashlytics  \|  Firebase](https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports-fabric-sdk?hl=ja)