Flutterで新規にアプリを作る時にテンプレ的に大体同じような事をやっているが、いつも忘れるので備忘録としてまとめ。
Flutterは絶賛開発中なので、本稿における記載は当時はこれでうまくいっていたぐらいに留めていただけると幸いです。
また、本稿は個人的なメモの意味合いが強いため、割愛している説明が多々あります。
Flutter2での動作確認と全編リファクタした新バージョンはこちら
環境
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
プロジェクトを作る。
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やPerformanceはとても便利なので裏方としてだけでも導入するのはオススメ。
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コマンドでダウンロードもできるので、スクリプトを書いておくと楽。
(このスクリプトでは後述するFlavor対応のために適切なディレクトリとファイル名で配置するようにしている。)
Androidの署名
keystoreはGUIから生成してもいいし、android - How can I create a keystore? - Stack Overflowを参考にして生成してもよい
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などを参考にして確認して、Firebaseに登録しておく。
keytool -list -v -alias key0 --keystore release.keystore
build.gradle
signingConfigs
にkeystoreを追記し、releaseビルド時に利用する。
keystoreのパスワードは必要に応じて隠そう。
参考: 今更ながらAndroid の keystore と 署名(signingConfigs) の管理・運用について考えてみた - Qiita
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
に修正
<application
android:label="@string/app_name">
dart-define
からの入力をパースしてアプリケーションIDのSuffixとアプリ名にセットする
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に従ってandroid/build.gradle
などに依存を追加していく。
次に以下のタスクを追加して、Flavorに応じたgoogle-services.json
を配置する。
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に追加しておく。
echo "android/app/google-services.json" >> .gitignore
iOS
Info.plistにAPP_NAME
をセット。
PRODUCT_NAME
にしておくとexportする際にRunner.ipa
で出力される
<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)
つまり以下のような変更になる
- 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をコピペする。
最後の処理のxcconfigに書き出す部分は適切に修正すること。
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
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を使った場合をコピペ。
作った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に追加しておく。
echo "ios/Runner/GoogleService-Info.plist" >> .gitignore
Runner.entitlements
Flavor毎にApp GroupsやAssociated Domainsを切り替える場合、./ios/Runner/Runner.entitlements
には次のように変数にしておき、dart-define
で渡す
<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=""
ではなく削除する)
flutter build ios \
--dart-define APP_NAME=devアプリ \
--dart-define APP_SUFFIX=.dev \
--dart-define APP_ENV=dev
Flutter側で値を使いたいならこのようにする。(finalではなくconstにすること)
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=""
ではなく削除する)
--dart-define APP_NAME=devアプリ --dart-define APP_SUFFIX=.dev --dart-define APP_ENV=dev
Configurationsもコミットする場合はgit add --force
が必要。
(flutter create
ではデフォルトで.idea/
がgitignoreされているので手動で追加する必要がある)
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
- dart-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける iOS編 - Qiita
- dart-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける Android編 - Qiita
- ビルド環境によるApp Groupsを変更する設定方法 - Qiita
輸出コンプライアンス
「いいえ」と答える場合はInfo.plistにあらかじめ追記しておくと手動でいいえする必要がないので楽。
+ <key>ITSAppUsesNonExemptEncryption</key>
+ <false/>
FlutterFire
導入したいPluginを選ぼう!!!
https://firebase.flutter.dev/
導入方法は各PluginのREADMEが最新で正確なので割愛。
Improve iOS Build Times
iOSのFirestore SDKはC++で50万行あるらしいので、普通に参照するとビルドに時間がかかる。
そこでプリコンパイルされたバージョンを参照すると大幅にビルド時間を短縮できるのでおすすめ。
FlutterFire Overview | FlutterFire
コードのカバレッジ計測
コードカバレッジ計測する場合、以下のように--coverage
を付与すればlcov.info
が出力される
flutter test --coverage --coverage-path=coverage/lcov.info
ただし、これはテストの対象になっているコードの結果のみが出力されるため、テストコードを全く書いていないとlcov.info
には何も出力されない。
そこで、全部のdartファイルをimportするテストコードを用意しておけば、flutter test --coverage
した時に全てのコードがlcov.info
に含まれるようになる
import 'package:example_app/hoge.dart';
void main() {}
手動でやるのは面倒なのでヘルプスクリプトで生成する
curl https://raw.githubusercontent.com/KoheiKanagu/dart_full_coverage/master/dart-coverage-helper | sh
gitignoreもしておく
echo "lcov.info" >> .gitignore
参考
Flutter test coverage will not report untested files · Issue #27997 · flutter/flutter
How can I generate test coverage of untested files on my flutter tests? - Stack Overflow
priezz/dart_full_coverage: Helper for full tests coverage checkup for you Dart/Flutter project
CICDについて
Github Actionsでの例を示す。
条件として、AndroidのビルドはGithubのubuntu-latest、iOSのビルドは私物のMacでself-hostedを利用する。
(Androidもself-hostedにしたければ指定するだけで対応できる)
そのため、iOSの証明書などはself-hostedのMacでよしなに整備されているものとする。
(要はプライベートリポジトリでGithub Actions使いたいけどmacOSのランナーはLinuxに比べてそこそこのお値段だから使ってないMacを活用しつつコストダウンという算段)
ジョブの流れ
詳細は割愛するが、git-flowに合わせる
- プルリク作成/更新されたら開始
- テスト実行
- Codecov
- Slackで通知
- プルリクがmasterかdevelopにマージされたら開始
- テスト実行
- Codecov
- バージョンのタグを貼る
- Releaseを作成
- release/*ブランチからmasterブランチにマージする際にはdevelopブランチに
#minor
と空コミット - Slackで通知
- ビルド(devのiOS・devのAndroid・prodのiOS・prodのAndroidをパラレルに実行)
- Release Assetに成果物(apk、aab、xcarchive、prodのipa)をアップロード
- App Distributionで配布
- 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.
Secrets
リポジトリのSecretsに以下をセットしておく
-
Slack API: Applications | Slackからアプリを作成してIncoming WebhooksのWebhook URLを
SLACK_WEBHOOK_URL
にセット -
Firebase CLI リファレンスを参考にしてトークンを取得して
FIREBASE_TOKEN
にセット(Firebase App Distributionを使わないなら不要) -
Codecovから
CODECOV_TOKEN
を取得してセット(Codecov使わないなら不要)
ビルドスクリプト
プロジェクトルートにscripts
というディレクトリを作って中に以下のファイルを入れておく(shは実行権限を忘れずにchmod +x ./scripts/*.sh
)
なおbuildiOS.shとbuildAndroid.shにおいては、APP_NAME
とAPP_SUFFIX
をよしなに変更すること。また、APP_ASSOCIATED_DOMAIN
は必要であれば。
AdHocExportOptions.plistとAppStoreExportOptions.plistにおいてはteamID
をよしなに変更すること。
Codecov
プロジェクトルートにcodecov.ymlを配置すれば、カバレッジには含めないファイルを指定できるなど、いろいろ設定できる。
例えば https://github.com/KoheiKanagu/my_flutter_app_template/blob/master/codecov.yml
workflow_dispatchでビルド
何らかの理由で手動でビルドしたい場合のジョブ
build_app_with_workflow_dispatch.yml
成果物の出力などはせず、ただビルドするだけ
アプリの標準言語を日本語にする
Xcodeでアプリの標準言語を日本語にする方法 - Qiita
記事ではdevelopmentRegion
はJapanese
にとあるがja
にすること
- developmentRegion = en
+ developmentRegion = ja
localizationsDelegates
GlobalWidgetsLocalizations
などを指定するならpubspec.yaml
に追記しておく
return MaterialApp(
localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
);
dependencies:
flutter_localizations:
sdk: flutter
intl: any
Firebase CrashlyticsのdSYM
dSYMをアップロードするスクリプト
uploadSymbols.sh