flutterでスマホアプリ開発するときに必ずやってること
macOS13.5.2、flutter3.22.0、xcode15.2、Android Studio Giraffeで検証
1. Bundle IDとアプリ名を本番、開発版で分ける
1端末に本番、開発版アプリを同居させたい。また、デバッグビルドの場合は開発版、リリースビルドの場合は本番版としたい。iOS、Androidの話
iOS
xcodeでios/Runner.xcworkspaceを起動
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app.dev
DISPLAY_NAME = Debug StartApp
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app
DISPLAY_NAME = StartApp
xcconfigのアプリ名を反映
<?xml version="1.0" encoding="UTF-8"?>
・・・
<dict>
・・・
<key>CFBundleDisplayName</key>
<string>$(DISPLAY_NAME)</string>
TARGETS > Runner > Build SettingsのProduct Bundle IdentifierのRunner列に$(PRODUCT_BUNDLE_IDENTIFIER)
を入力して、xcconfigのBundle IDを反映
before
編集
after
Android
本番用signingの準備
git管理対象外とする
# Android Signing
android/app/signing/key.jks
android/app/signing/signing.gradle
JKSファイルをプロジェクトに追加
android/app/signing/key.jks
signingConfigs {
release {
storeFile file("key.jks")
storePassword "xxxxx"
keyAlias "xxxxx"
keyPassword "xxxxx"
}
}
Bundle ID、アプリ名と本番ビルド時のsigningを設定
android {
・・・
defaultConfig {
・・・
applicationId "nrikiji.flutter_start_app"
・・・
}
buildTypes {
debug {
debuggable true
applicationIdSuffix ".dev"
resValue "string", "app_name", "Debug StartApp"
}
release {
debuggable false
applicationIdSuffix ""
resValue "string", "app_name", "StartApp"
apply from: './signing/signing.gradle', to: android
signingConfig signingConfigs.release
}
}
上で設定したアプリ名を使用するようにする
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="nrikiji.flutter_start_app">
<application
android:label="@string/app_name"
・・・
2. firebaseの設定ファイルを本番、開発版で分ける
firebaseのanalyticsとcrashlyticsはどのアプリでも最低限使いたい。また、firebaseプロジェクト、アプリは作成済みとする。iOS、Androidとflutterの話
iOS
firebase consoleよりGoogleService-Info.plistをダウンロードして、ios/Runner以下に配置する。開発、本番用で異なるファイル名とする
$ ls -l
ios/Runner/GoogleService-Info-dev.plist
ios/Runner/GoogleService-Info-prod.plist
git管理対象外とする
# Firebase Settings Files
ios/Runner/GoogleService-Info-dev.plist
ios/Runner/GoogleService-Info-prod.plist
TARGETS > Runner > Build PhasesのRun Scriptを設定してGoogleService-Info.plistを開発、本番ビルドで切り替える
if [ "${CONFIGURATION}" == "Debug" ]; then
cp -r "${PROJECT_DIR}/Runner/GoogleService-Info-dev.plist" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"
elif [ "${CONFIGURATION}" == "Release" ]; then
cp -r "${PROJECT_DIR}/Runner/GoogleService-Info-prod.plist" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"
fi
Android
firebase consoleよりgoogle-services.jsonをダウンロードして、android/app/src以下のdebug、release(ディレクトリ作成)以下に配置する。
$ ls -l
android/app/src/debug/google-services.json
android/app/src/release/google-services.json
git管理対象外とする
# Firebase Settings Files
android/app/src/debug/google-services.json
android/app/src/release/google-services.json
gradleプラグインを追加
buildscript {
・・・
dependencies {
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0'
・・・
}
}
・・・
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
pubspec.yamlにfirebase関連のプラグイン追加
dependencies:
flutter:
・・・
firebase_core: ^1.17.1
firebase_analytics: ^9.1.9
firebase_crashlytics: ^2.8.1
flutterでfirebaseの初期化
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
void main() {
runZonedGuarded(
() {
WidgetsFlutterBinding.ensureInitialized();
return runApp(ProviderScope(child: MyApp()));
},
(e, st) {
FirebaseCrashlytics.instance.recordError(e, st);
},
);
}
class MyApp extends StatelessWidget {
final _initialization = Firebase.initializeApp();
Widget build(BuildContext context) {
return FutureBuilder(
future: _initialization,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done || snapshot.hasError) {
return MaterialApp(・・・);
} else {
return SizedBox.shrink();
}
});
}
}
3. 本番、開発版でプログラムで使用する定数を切り替える
Web APIの接続先URLなどを本番、開発版で別としたい。また、デバッグビルドの場合は開発用、リリースビルドの場合は本番用の定数を使用したい。flutterの話
AndroidStudioビルド設定
Android Studioの「Edit Configurations」よりdebugとreleaseを追加する
debug: 「Addional arguments」に「--dart-define env=dev」を設定
release: 「Addional arguments」に「--release --dart-define env=prod」を設定
※コマンドラインでビルドする場合
# Debug
$ flutter build ios --dart-define=env=dev
# Release(iOS)
$ flutter build ios --release --dart-define=env=prod
# Release(Android)
$ flutter build appbundle --release --dart-define=env=prod --no-shrink
各環境の定数定義
class Environment {
final EnvironmentKind kind;
final String baseApiUrl;
factory Environment() {
const env = String.fromEnvironment('env');
return env == 'prod' ? Environment.prod() : Environment.dev();
}
const Environment._({
this.kind,
this.baseApiUrl,
});
const Environment.prod()
: this._(
kind: EnvironmentKind.Prod,
baseApiUrl: "https://example.com",
);
const Environment.dev()
: this._(
kind: EnvironmentKind.Dev,
baseApiUrl: "https://dev.example.com",
);
}
enum EnvironmentKind {
Dev,
Prod,
}
4. ローカライズ
日本語と英語くらいは最低限対応したアプリを作りたい
dependencies:
・・・
flutter_localizations:
sdk: flutter
import 'package:flutter/cupertino.dart';
class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const AppLocalizationsDelegate();
bool isSupported(Locale locale) => ['en', 'ja'].contains(locale.languageCode);
Future<AppLocalizations> load(Locale locale) async => AppLocalizations(locale);
bool shouldReload(AppLocalizationsDelegate old) => false;
}
class AppLocalizations {
final LocalizeMessages messages;
AppLocalizations(Locale locale) : this.messages = LocalizeMessages.of(locale);
static LocalizeMessages of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!.messages;
}
}
class LocalizeMessages {
final String greet;
LocalizeMessages({
required this.greet,
});
factory LocalizeMessages.of(Locale locale) {
switch (locale.languageCode) {
case 'ja':
return LocalizeMessages.ja();
case 'en':
return LocalizeMessages.en();
default:
return LocalizeMessages.en();
}
}
factory LocalizeMessages.ja() => LocalizeMessages(
greet: 'こんにちは',
);
factory LocalizeMessages.en() => LocalizeMessages(
greet: 'Hello',
);
}
5. GitHub Actionsでストアへバイナリアップロード
ビルドミス防止と1度おぼえたら手動でアップロードするのがだるくなった。また、App Store Connect API と Google Play Developer Publishing API は有効化済みとする
以下のファイルを用意して、GitHub Actions secretsに必要な値を設定。git tagのプッシュをトリガーにworkflowが実行されるようにする。
workflow
.github/workflows/release.yml
fastlane
Gemfile
ios/fastlane/Appfile
ios/fastlane/Fastfile
GitHub Actions secrets
こちらを参考
まとめ
ここで書いたことを git clone で始めることができるスタートアッププロジェクト。flutterのバージョンアップで大きな変更があった場合にこのプロジェクトを1から作り直す手順を作ることが実はこの記事を書いた主目的。使用手順はREADMEにまとめた
Discussion
DEBUG・RELEASEを開発・本番に紐づけてしまうとリリースビルドでしか発生しないバグを検証しづらくなってしまうので、開発・本番をわけるのは flutter_flavorizr などで明示的に分けたほうが良いと思います。
Run scriptのところを{PROJECT_DIR}/Runner/GoogleService-Info-dev.plist" " BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"{PROJECT_DIR}/Runner/GoogleService-Info-prod.plist" " BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"{PROJECT_DIR}/Runner/GoogleService-Info-dev.plist" " BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"{PROJECT_DIR}/Runner/GoogleService-Info-prod.plist" " BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"
if [ "${CONFIGURATION}" == "Debug" ]; then
cp -r "
elif [ "${CONFIGURATION}" == "Release" ]; then
cp -r "
fi
だけにしたら、'Flutter/Flutter.h' file not foundが出まくりました笑
ちゃんと
if [ "${CONFIGURATION}" == "Debug" ]; then
cp -r "
elif [ "${CONFIGURATION}" == "Release" ]; then
cp -r "
fi
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
まで記述しないとダメでしたね。うっかりしてました笑笑