🛫

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

commits22 min read

はじめに

Flutterで新規にアプリを作る時にテンプレ的に大体同じような事をやっているが、いつも忘れるので備忘録としてまとめ。
Flutterは絶賛開発中なので、本稿の内容は当時これでうまくいっていたぐらいに留めていただけると幸いです。
また、本稿は個人的なメモの意味合いが強いため、割愛している説明が多々あります。

本稿の変更を適用したサンプルリポジトリはこちら

記事もテンプレ自体も随時更新予定

旧バージョンの記事はこちら:Flutterで新規アプリ作る時にやることまとめ - Qiita

この記事でできること要約

  • 大体アプリ作るときにやるであろう手順の備忘録
  • dart-defineを用いて開発環境、ステージング環境、本番環境のフレーバーを切り替え、別々のアプリとして独立させる方法
  • フレーバーに応じて利用するFirebaseプロジェクトを切り替える方法
  • コードカバレッジ計測のTips
  • GitHub Actionsを用いて、Firebase App Distributionでアプリを配布する方法
  • GitHubリリースから各ストアに申請できる.ipa.aabをダウンロードできる方法
  • セマンティックバージョニングでアプリのバージョンを管理する方法

環境

Flutter 2.0.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 1d9032c7e1 (4 days ago) • 2021-04-29 17:37:58 -0700
Engine • revision 05e680e202
Tools • Dart 2.12.3

Flutter Create

プロジェクトを作る。

flutter create --org com.example your_project_name

orgはパッケージ名やAndroidのディレクトリ構造などに影響するため、後から変更するのは面倒なので注意。

Firebaseプロジェクト作る

Firebase コンソールから作成する。
アプリを実行してインストールを確認という疎通確認のステップはとりあえず無視。

iOSとAndroidアプリをそれぞれ追加し、アプリIDは後ほど使うのでメモしておく。

FirestoreやApp Distributionを利用するなら予めFirebaseコンソールから有効にしておくことを忘れずに。

開発環境や本番環境などのフレーバー毎にそれぞれプロジェクトを作成しておくこと。

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

東京はasia-northeast1

Firebaseの構成ファイル

構成ファイルはブラウザからポチッとダウンロードしてもいいが、firebase コマンドでダウンロードもできるので、スクリプトを書いておくと楽。
(このスクリプトでは後述するFlavor対応のために適切なディレクトリとファイル名で配置するようにしている)

iOS

CFBundleNameとCFBundleDisplayName

Info.plistのCFBundleNameとCFBundleDisplayNameを変更する。
PRODUCT_NAMEにしておくとexportする際にRunner.ipaとファイル名が固定されるので都合がいい。

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

輸出コンプライアンス

「いいえ」と答える場合はInfo.plistにあらかじめ追記しておくと手動でいいえする必要がないので楽。

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

スクショ

Bundle IdentifierのSuffix

RunnerのBuild SettingsからProduct Bundle Identifierを検索して以下のようにAPP_SUFFIXを追記。

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

dart-defineからの変数を読み込む下準備

次にRunnerのSchemeをEditして、BuildのPre-actionsに次のRunScriptを追加する。

function urldecode() {
    : "${*//+/ }"
    echo "${_//%/\\x}"
}

IFS=',' read -r -a define_items <<<"$DART_DEFINES"

for index in "${!define_items[@]}"; do
    define_items[$index]=$(urldecode "${define_items[$index]}")
done

printf "%s\n" "${define_items[@]}" | grep '^APP_' >>${SRCROOT}/Flutter/Generated.xcconfig

Provide build settings fromはRunnerを選択する。

Provisioning Profile

マニュアルでProvisioning Profileを管理したいのでSigningのAutomatically manage signingのチェックを外す。

スクショ

Build SettingsからProvisioning Profile(PROVISIONING_PROFILE_SPECIFIER)を次のように変更する。

ios/Runner.xcodeproj/project.pbxproj
- PROVISIONING_PROFILE_SPECIFIER = "";
+ PROVISIONING_PROFILE_SPECIFIER = "$(APP_PROVISIONING_PROFILE_SPECIFIER)";

Build SettingsからDevelopment Team(DEVELOPMENT_TEAM)を適切なチームに変更する。

ios/Runner.xcodeproj/project.pbxproj
+ DEVELOPMENT_TEAM = XXXXXXXXXX;

Build SettingsからCode Signing Identity(CODE_SIGN_IDENTITY)を全て利用するProvisioning Profileに関連したApple Distributionへ変更する。

Apple Distributionでも問題なくビルドが通る場合もあるが、あくまでApple Distributionは証明書を自動で選択するものなので、たまたまうまく行っているに過ぎない。

ios/Runner.xcodeproj/project.pbxproj
+ CODE_SIGN_IDENTITY = "Apple Distribution: Kohei Kanagu (4XBP3H82S7)";

GoogleService-Info.plist

FirebaseのSDKはios/Runner/GoogleService-Info.plistを自動で読み込むのでフレーバーに応じて差し替えてあげる必要がある。

まず適当な内容でGoogleService-Info.plistを作って、XcodeのProject NavigatorでRunnerディレクトリ下にドラッグアンドドロップしてGoogleService-Info.plistへの参照を作っておく。

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には以下を追加し、Copy Bundle Resourcesよりも上に配置する。

$SRCROOT/Runner/GoogleService-Info.plist

最後に、ios/Runner/GoogleService-Info.plistをgitignoreに追加する。

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

Runner.entitlements

フレーバー毎にAssociated Domainsを切り替えるため、com.apple.developer.associated-domainsの値は変数にしておき、後述のdart-defineで渡す。

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

App Groupsなどでも同様。

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

ios/Runner.xcodeproj/project.pbxproj
- developmentRegion = en;
+ developmentRegion = ja;
  knownRegions = (
-     en,
+     ja,
      Base,
  );

サポートするiOSバージョン

RunnerのInfoのiOS Deployment TargetAppleが算出しているシェア率を参考にして変更する。

ios/Podfileの最上部にコメントアウトしてあるplatformも同じ値に変更。

ios/Podfile
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0' ←これ

最下部のpost_installの中身を次のようにIPHONEOS_DEPLOYMENT_TARGETを指定する。

ios/Podfile
post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.3'
    end
  end
end

なお、Flutterプロジェクト作りたての時点ではios/Podfileが存在しないため、例えば次のように一旦ビルドすると生成される。

flutter build ios --config-only

Bitcodeのサポート

Build SettingsからEnable BitcodeYESにする。

iOSの参考文献

Android

署名

keystoreはAndroidStudioで生成するか、次のように作成する。

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に置いておく。

なお2つ目のコマンドを実行するとrelease.keystore.oldがバックアップとして生成されるが特に使わないので削除しても良い。

SHAは次のように求めてFirebaseに登録しておく。

keytool -list -v -alias key0 --keystore release.keystore

build.gradle

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

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

buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}

アプリ名

labelを次のように修正。

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

dart-defineからの入力をパースしてセットする。

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
    }
...
}

google-services.json

FirebaseのSDKはandroid/app/google-services.jsonを自動で読み込むのでフレーバーに応じて差し替えてあげる必要がある。

以下のタスクを追加して、フレーバーに応じたgoogle-services.jsonを配置する。

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に追加しておく。

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

サポートするAndroidバージョン

Flutter 2.0.6時点ではminSdkVersionの初期値は16だが、いくらなんでも低すぎるので上げる。
少なくともgoogle_mobile_adsなどは19以上である必要がある。

シェア5%を切ったら切るぐらいか。

Androidの参考文献

dart-define

前述してきたフレーバー毎に変えるAPP_*のような変数は、dart-defineでiOS、Androidそれぞれビルドする際に適用する。

flutter build

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

flutter build ios \
    --dart-define APP_NAME=devアプリ \
    --dart-define APP_SUFFIX=.dev \
    --dart-define APP_ENV=dev \
    --dart-define APP_ASSOCIATED_DOMAIN=applinks:com.example.page.link

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');

Visual Studio Code

launch.json の例

{
  "name": "dev",
  "request": "launch",
  "program": "lib/main.dart",
  "type": "dart",
  "args": [
    "--dart-define=APP_NAME=devアプリ",
    "--dart-define=APP_SUFFIX=.dev",
    "--dart-define=APP_ENV=dev",
    "--dart-define=APP_ASSOCIATED_DOMAIN=applinks:com.example.page.link"
  ]
}

FlutterFire

FlutterFire Overview | FlutterFireを参考にして導入する。
iOS/Androidそれぞれのプラットフォーム別のセットアップも忘れずに。

Crashlytics

iOSでCrashlyticsを導入する際、${PODS_ROOT}/FirebaseCrashlytics/runのScriptは最後に配置する。
スクショ

参考:Firebase Crashlytics を使ってみる

dSYMの手動アップロード

Bitcodeを有効にしていた場合は、App StoreからdSYMをダウンロードして手動でアップロードする必要がある。
dSYMをアップロードするスクリプト

参考:Firebase Crashlytics SDK を使用して難読化解除されたクラッシュ レポートを取得する

FirestoreのImprove iOS Build Times

iOSのFirestore SDKはC++で50万行あるらしいので、普通に参照するとビルドに時間がかかる。
そこでプリコンパイルされたバージョンを参照すると大幅にビルド時間を短縮できるのでおすすめ。
参考:FlutterFire Overview | FlutterFire

コードカバレッジ計測

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

flutter test --coverage

ただし、これはテストの対象になっているコードの結果のみが出力されるため、テストコードを全く書いていないとlcov.infoには何も出力されない。
つまり全部のdartファイルをimportするだけのテストを用意しておけば、全てのコードがlcov.infoに含まれる。

test/coverage_test.dart のようなもの
import 'package:example_app/hoge1.dart';
import 'package:example_app/hoge2.dart';
import 'package:example_app/hoge3.dart';

void main() {}

手動で追記していくのは面倒なのでヘルプスクリプトで生成する。

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

gitignoreもしておく。

echo "coverage/lcov.info" >> .gitignore

コードカバレッジ計測の参考文献

GitHub Actions

ご存知の通りmacOSのランナーはそこそこのお値段なので、必要に応じてself-hostedにすると良い。

ワークフローはこちら→.github/workflows
FIXMEとしている部分は適切な値に変更すること。

想定している開発の流れ

  • developがメインのブランチ
  • developmainへの直コミットは原則禁止

通常開発の流れ

  1. 開発開始
  2. developからfeature/hogeチェックアウト
  3. 開発
  4. feature/hogeからdevelopにプルリク
  5. マージ
  6. しばらくするとGitHubのReleasesがバージョン名で作成される
  7. しばらくするとReleasesのAssetsに成果物がアップロードされる
  8. しばらくすると各フレーバー(dev,stg,prod)のアプリがFirebase App Distributionから配布される

リリース作業の流れ

  1. developからrelease/hogeチェックアウト
  2. release/hogeからmainにプルリク
  3. マージ
  4. しばらくするとGitHubのReleasesがバージョン名で作成される
  5. しばらくするとdevelopに空コミットが入る(バージョニングのため)
  6. しばらくするとReleasesのAssetsに成果物がアップロードされる
  7. しばらくすると各フレーバー(dev,stg,prod)のアプリがFirebase App Distributionから配布される
  8. AssetsからiOS_prod_appStore.ipa.tar.gzAndroid_prod.aabを手動でダウンロード
  9. aabはGoogle Play Consoleにブラウザから直接、ipaはTransporterでアップロード
  10. iOSの場合はApple側の処理が完了次第、dSYMをダウンロードしてFirebaseにアップロード(./scripts/uploadSymbols.shが便利かも)

HotFixの流れ

  1. mainからhotfix/hogeチェックアウト(release/hogeにしてしまうとminorバージョン番号が無意味にbumpしてしまう)
  2. hotfix/hogeからmainにプルリク
  3. 以降リリース作業と同じ

pull_requests.ymlの概要

  1. プルリク作成/更新されたら開始
  2. テスト実行
  3. Codecov
  4. Slackで通知

releases.ymlの概要

  1. プルリクがmainかdevelopにマージされたら開始
  2. テスト実行
  3. Codecov
  4. バージョンのタグを貼る
  5. GitHubのReleaseを作成
  6. release/*ブランチからmainにマージされた際にはdevelopに#minorと空コミット
  7. Slackで通知
  8. ビルド(各フレーバーでflutter buildをパラレルに実行)
  9. Release Assetに成果物(apk、aab、ipa)をアップロード
  10. App Distributionで配布
  11. Slackで通知

manual_build.ymlの概要

検証など何らかの理由で任意のタイミングでビルドするワークフローを実行したい場合に利用する。
.github/workflows/manual_build.yml

ただビルドするだけなので、成果物の保存などはしない。

Actions secrets

SLACK_WEBHOOK_URL

ワークフローの結果をSlackに通知する。
Slack API: Applications | Slackからアプリを作成してIncoming WebhooksのWebhook URLを設定する。

FIREBASE_TOKEN

Firebase App Distributionで利用する。
Firebase CLI リファレンスを参考にしてトークンを取得し設定する。

CODECOV_TOKEN

カバレッジを計測するために利用する。
Codecovからトークンを取得し、設定する。

MOBILEPROVISION_BASE64_*

iOSアプリを実機で動かすためのProvisioning Profileに利用する。
Certificates, Identifiers & Profiles - Apple Developerで各フレーバーのAd Hoc向け、App Store向けのProvisioning Profileを作成する。

スクショ

この時のProvisioning Profile Name.github/workflows/scripts/buildiOS.shAPP_PROVISIONING_PROFILE_SPECIFIERで指定するものと同じ名前にすること。

それぞれダウンロードして次のようにBase64にエンコード、下記の名前でそれぞれSecretsに設定する。

base64 devkingumyFlutterAppTemplatedev_Ad_Hoc.mobileprovision | pbcopy
  • MOBILEPROVISION_BASE64_DEV
  • MOBILEPROVISION_BASE64_STG
  • MOBILEPROVISION_BASE64_PROD
  • MOBILEPROVISION_BASE64_APP_STORE

P12_BASE64

iOSアプリをApp StoreやAd Hocで配布できるApple Distributionなp12証明書を用意し、次のようにBase64にエンコードしたものを設定する。

base64 hoge.p12

P12_PASSWORD

p12証明書を書き出した時のパスワードを設定する。

ワークフローで使うスクリプト群

.github/workflows/scripts
必要に応じて実行権限を忘れずに。
FIXMEとある箇所に関しては適時変更すること。

chmod +x .github/workflows/scripts/*.sh

Codecov

プロジェクトルートにcodecov.ymlを配置すれば、カバレッジには含めないファイルを指定できるなど、いろいろ設定できる。
例:codecov.yml

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

fvm

Flutterのバージョンはfvmで管理する。
gitignoreへの追加と.vscode/settings.jsonの修正も忘れずに。

Configuration | Flutter Version Management

echo ".fvm/flutter_sdk" >> .gitignore

アプリのアイコン

flutter_launcher_icons | Dart Packageを利用する方法もあるが、バージョン0.9.0時点ではAndroidのアダプティブアイコン周りで不具合があるため微妙。

どうせ頻繁に変えるものでもないので、Android Studioで生成したり適当なジェネレータを利用した方が良い。