Flutter: プロジェクトデプロイ — ShopAppローンチとリリース
ローンチは終わりではなく — 継続的デリバリーの始まりです。
📋 前提条件: 以下を先にマスターしている必要があります
- レッスン27:プロジェクト開発 — ShopApp機能実装
1. このレッスンで学ぶこと
- ビルド最適化:Tree Shaking / コード分割 / アセット圧縮 / R8難読化
- Androidデプロイ:署名キー + App Bundle + Google Play公開
- iOSデプロイ:証明書設定 + TestFlight + App Store審査
- Webデプロイ:Firebase Hosting + PWA + SEO最適化
- 運用監視:Crashlyticsクラッシュ収集 + Performance ネットワークトレーシング + App Center配布
2. ローンチ当日クラッシュのストーリー
(1) 悩み:開発では動くが本番で5%クラッシュ率
Bobは4ヶ月かけてShopAppを開発。ローンチ当日、クラッシュ率は5%。原因:Android R8難読化がFirestoreシリアライゼーションクラス名を削除(デシリアライゼーション失敗)、iOSはカメラ権限説明文の欠落で却下、Web版ホームページの読み込みに8秒(コード分割なし)。緊急修正 + 再審査にさらに2週間。
(2) ビルド最適化 + リリース前チェックリストソリューション
ローンチ前にビルド最適化(難読化ルール、コード分割、アセット圧縮)+ リリースチェックリスト(権限、署名、ProGuardルール、TestFlight事前審査)を実行し、初回品質を確保。
(3) 成果:初回ローンチクラッシュ率 < 0.1%
2回目のプロジェクトでチェックリストを使用後、Bobは初回クラッシュ率0.08%を達成、初回審査通過、WebのFirst Contentful Paintは1.5秒。
3. ビルド最適化
(1) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Android ProGuard / R8ルール
PROGUARD
# flutter_proguard_rules.pro
# Keep Firestore model classes (used by json_serializable)
-keepclassmembers class com.shopapp..model. {
<fields>;
}
# Keep freezed generated classes
-keep class com.shopapp...freezed.dart { *; }
# Keep Firebase classes
-keep class io.flutter.plugins.firebase.** { *; }
-keep class com.google.firebase.** { *; }
# Keep Stripe SDK
-keep class com.stripe.android.** { *; }
# Keep Google Sign-In
-keep class com.google.android.gms.auth.** { *; }
# Remove logging in release
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Flutterビルド設定
YAML
# pubspec.yaml build optimization
flutter:
uses-material-design: true
# Exclude unused locales
generate: true
# android/app/build.gradle
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
// Split per ABI to reduce APK size
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
universalApk false
}
}
}
(3) ビルド最適化比較
| 最適化項目 | 最適化前 | 最適化後 | 削減率 |
|---|---|---|---|
| APKサイズ(arm64) | 28 MB | 12 MB | 57% |
| Web First Contentful Paint | 8.2 s | 1.5 s | 82% |
| Dartコードサイズ | 6.4 MB | 2.1 MB | 67% |
| 起動時間(コールド) | 3.5 s | 1.8 s | 49% |
4. Androidデプロイ
(1) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:署名キー設定
BASH
# Generate signing keystore
keytool -genkey -v -keystore shopapp-release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias shopapp
# Create key.properties
cat > android/key.properties << 'EOF'
storePassword=STORE_PASSWORD
keyPassword=KEY_PASSWORD
keyAlias=shopapp
storeFile=../shopapp-release.jks
EOF
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:build.gradle署名統合
GROOVY
// android/app/build.gradle
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:App Bundleビルドとアップロード
BASH
# Build App Bundle (recommended over APK for Play Store)
flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info
# Upload to Google Play Console
# Option 1: Manual via Play Console UI
# Option 2: Fastlane
cd android && fastlane supply --aab ../build/app/outputs/bundle/release/app-release.aab --track internal
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(4) Androidリリースチェックリスト
| # | チェック項目 | ステータス |
|---|---|---|
| 1 | 署名キーが生成・バックアップ済み | ☐ |
| 2 | key.propertiesがバージョン管理に含まれていない | ☐ |
| 3 | ProGuardルールが全サードパーティSDKをカバー | ☐ |
| 4 | versionCode / versionNameが更新済み | ☐ |
| 5 | 権限宣言が最小化されている | ☐ |
| 6 | App Bundle(APKではなく)でアップロード | ☐ |
| 7 | ストアスクリーンショット + 説明が準備済み | ☐ |
| 8 | プライバシーポリシーURLが設定済み | ☐ |
5. iOSデプロイ
(1) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Info.plist権限設定
XML
<!-- ios/Runner/Info.plist -->
`<key>`NSCameraUsageDescription</key>
`<string>`ShopApp needs camera access to scan product barcodes.</string>
`<key>`NSPhotoLibraryUsageDescription</key>
`<string>`ShopApp needs photo access to upload product reviews.</string>
`<key>`NSLocationWhenInUseUsageDescription</key>
`<string>`ShopApp uses location for local delivery estimates.</string>
`<key>`CFBundleURLTypes</key>
`<array>`
`<dict>`
`<key>`CFBundleURLSchemes</key>
`<array>`
`<string>`com.googleusercontent.apps.YOUR_CLIENT_ID</string>
</array>
`</dict>`
</array>
(2) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Fastlane iOSリリース
RUBY
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
include_bitcode: false,
export_options: {
compileBitcode: false,
provisioningProfiles: {
"com.shopapp.app" => "match AppStore com.shopapp.app"
}
}
)
upload_to_testflight(
skip_waiting_for_build_processing: true
)
end
desc "Submit to App Store review"
lane :release do
beta
deliver(
submit_for_review: true,
automatic_release: true,
force: true,
metadata_path: "./metadata"
)
end
end
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) iOS審査却下の一般的な理由
| 却下理由 | 解決策 |
|---|---|
| 権限説明文の欠落 | Info.plistにNS*UsageDescriptionを追加 |
| 仮想商品でIAP未使用 | 仮想商品はStoreKit、物理商品はStripeを使用 |
| ログインでソーシャルアカウント連携を強制 | メール/パスワードログインオプションを提供 |
| WebViewラッパーアプリ | ネイティブ機能 > 50%を確保 |
| メタデータ不完全 | 年齢レーティング、プライバシーポリシーURLを記入 |
6. Webデプロイ
(1) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Firebase Hosting設定
JSON
// firebase.json
{
"hosting": {
"public": "build/web",
"ignore": ["firebase.json", "/.*", "/node_modules/**"],
"rewrites": [
{ "source": "**", "destination": "/index.html" }
],
"headers": [
{
"source": "**/*.@(js|css|wasm)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "**/*.@(jpg|jpeg|gif|png|svg|webp|ico)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=604800" }
]
},
{
"source": "/index.html",
"headers": [
{ "key": "Cache-Control", "value": "no-cache" }
]
}
]
}
}
(2) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Webビルドとデプロイ
BASH
# Build web with CanvasKit renderer (better performance)
flutter build web --release --web-renderer canvaskit --pwa-strategy offline_first
# Deploy to Firebase Hosting
firebase deploy --only hosting
# Or deploy to custom server with Nginx
scp -r build/web/* user@server:/var/www/shopapp/
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) Webパフォーマンス最適化比較
| 最適化戦略 | First Paint | LCP | FID |
|---|---|---|---|
| 最適化なし(HTMLレンダラー) | 8.2 s | 7.8 s | 180 ms |
| CanvasKitレンダラー | 3.1 s | 2.8 s | 45 ms |
| + コード分割 + 遅延読み込み | 1.5 s | 1.2 s | 30 ms |
| + PWA + Service Worker | 0.8 s(2回目以降) | 0.5 s | 15 ms |
7. 運用監視
(1) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Crashlytics連携
DART
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:ui' show PlatformDispatcher;
// Dependencies: firebase_core: ^2.0.0, firebase_crashlytics: ^3.0.0, firebase_auth: ^4.0.0
// DefaultFirebaseOptions / ShopApp come from the ShopApp project
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Crashlytics
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
// Pass all uncaught errors to Crashlytics
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
// Set user identifier for crash reports
FirebaseAuth.instance.authStateChanges().listen((user) {
if (user != null) {
FirebaseCrashlytics.instance.setUserIdentifier(user.uid);
}
});
// Custom keys for context
FirebaseCrashlytics.instance.setCustomKey('app_version', '1.0.0');
FirebaseCrashlytics.instance.setCustomKey('environment', 'production');
runApp(const ShopApp());
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) ▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Performance Monitoring
DART
import 'package:dio/dio.dart';
import 'package:firebase_performance/firebase_performance.dart';
// Dependencies: firebase_performance: ^0.9.0, dio: ^5.0.0 (add to dependencies)
class ShopPerformance {
static final _perf = FirebasePerformance.instance;
static Future<void> trackProductLoad(String productId) async {
final trace = _perf.newTrace('product_load');
trace.putAttribute('product_id', productId);
await trace.start();
try {
// ... load product ...
} finally {
await trace.stop();
}
}
static Future<void> trackSearch(String query, int results) async {
final trace = _perf.newTrace('search');
trace.putAttribute('query', query);
trace.putMetric('results_count', results);
await trace.start();
await trace.stop();
}
static Future<void> trackCheckout(double total) async {
final trace = _perf.newTrace('checkout');
trace.putMetric('order_total_cents', (total * 100).toInt());
await trace.start();
await trace.stop();
}
}
// HTTP performance auto-tracking with Dio
class PerformanceInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.extra['trace'] = FirebasePerformance.instance.newHttpMetric(
options.uri.toString(), HttpMethod.Post);
(options.extra['trace'] as HttpMetric).start();
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final trace = response.requestOptions.extra['trace'] as HttpMetric?;
trace?.httpResponseCode = response.statusCode;
trace?.stop();
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final trace = err.requestOptions.extra['trace'] as HttpMetric?;
trace?.httpResponseCode = err.response?.statusCode;
trace?.stop();
handler.next(err);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完全な例:リリース前自動チェックスクリプト
DART
// tool/release_check.dart
import 'dart:io';
// Pure Dart script, no Flutter dependency, run with dart run tool/release_check.dart
enum Platform { android, ios, web }
class ReleaseChecker {
static Future<bool> run(Platform platform) async {
final results = <String, bool>{};
results['version_updated'] = _checkVersionUpdated();
results['changelog_updated'] = _checkChangelog();
results['no_debug_print'] = _checkNoDebugPrint();
results['proguard_rules'] = platform == Platform.android ? _checkProguard() : true;
results['info_plist'] = platform == Platform.ios ? _checkInfoPlist() : true;
results['privacy_policy'] = _checkPrivacyPolicy();
results['signing_config'] = _checkSigningConfig(platform);
results['test_pass'] = await _runTests();
results['analyze_pass'] = await _runAnalyze();
final allPass = results.values.every((v) => v);
if (!allPass) {
print('Release check FAILED:');
results.forEach((k, v) { if (!v) print(' ✗ $k'); });
} else {
print('All release checks PASSED!');
}
return allPass;
}
static bool _checkVersionUpdated() {
// Compare current pubspec.yaml version with last git tag
return true; // Simplified
}
static bool _checkChangelog() => File('CHANGELOG.md').existsSync();
static bool _checkNoDebugPrint() {
// Grep for debugPrint / print() in lib/
return true; // Simplified
}
static bool _checkProguard() => File('android/app/proguard-rules.pro').existsSync();
static bool _checkInfoPlist() {
final plist = File('ios/Runner/Info.plist').readAsStringSync();
return plist.contains('NSCameraUsageDescription') && plist.contains('NSPhotoLibraryUsageDescription');
}
static bool _checkPrivacyPolicy() => true;
static bool _checkSigningConfig(Platform platform) {
if (platform == Platform.android) return File('android/key.properties').existsSync();
return true; // iOS uses Xcode managed signing
}
static Future<bool> _runTests() async {
final result = await Process.run('flutter', ['test']);
return result.exitCode == 0;
}
static Future<bool> _runAnalyze() async {
final result = await Process.run('flutter', ['analyze']);
return result.exitCode == 0;
}
}
void main(List<String> args) async {
final platform = switch (args.firstOrNull) {
'android' => Platform.android,
'ios' => Platform.ios,
'web' => Platform.web,
_ => Platform.android,
};
final pass = await ReleaseChecker.run(platform);
exit(pass ? 0 : 1);
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
9. デプロイプロセス概要
graph TD
CODE[Source Code] --> ANALYZE[flutter analyze]
ANALYZE --> TEST[flutter test]
TEST --> CHECK{Release Check}
CHECK -->|FAIL| FIX[Fix Issues]
FIX --> ANALYZE
CHECK -->|PASS| BUILD[flutter build]
BUILD --> ANDROID[Android: AAB → Play Console]
BUILD --> IOS[iOS: IPA → TestFlight → App Store]
BUILD --> WEB[Web: Firebase Hosting]
ANDROID --> MONITOR[Crashlytics + Performance]
IOS --> MONITOR
WEB --> MONITOR
MONITOR --> ITERATE[Iterate & Release Next Version]
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q App BundleとAPKの違いは何ですか?
A AABはGoogle Playの公開形式で、デバイスのABI/密度に応じて自動分割してダウンロードサイズを小さくします。APKはサイドローディング用の完全なインストールパッケージです。
Q iOS審査は通常どのくらいかかりますか?
A 初回提出は2-5日、更新は1-2日。却下後の修正と再提出にはさらに1-3日かかります。
Q Flutter WebはSEOに対応していますか?
A Flutter WebはSPA — 検索エンジンはコンテンツのインデックスが困難。解決策:1) SSR使用(dart2js + サーバー)、2) 主要ページをプリレンダリング、3) 静的ランディングページで補完。
Q 難読化後のCrashlyticsスタックトレースが読めない場合は?
A ビルド時に--split-debug-infoでシンボルテーブルを保存し、難読化解除ファイル(Android)またはdSYM(iOS)をCrashlyticsにアップロードしてスタックトレースを復元してください。
Q Flutterでホットアップデート/Code Pushはサポートされていますか?
A Flutterはネイティブでコードホットアップデートをサポートしていません。shorebird.devで部分コードプッシュが可能(Androidのみ)、iOSは依然としてApp Store審査が必要。
Q マルチプラットフォーム同時リリースのバージョン管理はどうしますか?
A 統一されたpubspec.yamlバージョンを使用し、CI/CDマトリックスビルド(GitHub Actions:android + ios + webジョブを並列実行)で対応。
Q ローンチ後のクラッシュ率はどの程度が正常ですか?
A 優秀 < 0.1%、良好 < 0.5%、要注意 > 1%。Crashlyticsがリアルタイムクラッシュ率ダッシュボードを提供。
📖 まとめ
- ビルド最適化:R8難読化 + ABI分割 + CanvasKitレンダラー — APK57%削減、Web初回描画82%削減
- Android:署名キー → App Bundle → Play Console → internal/beta/productionロールアウト
- iOS:Info.plist権限 → Fastlane → TestFlight → App Store審査
- Web:Firebase Hosting + PWA + Service Worker + CDNキャッシュ戦略
- 運用:Crashlyticsクラッシュ収集 + Performance ネットワークトレーシング + 自動リリースチェック
📝 練習問題
- 基本 (⭐):Android署名キー + ProGuardルールを設定し、Release App Bundleをビルドして、サイズ最適化を確認してください。
- 中級 (⭐⭐):Crashlytics + Performance Monitoringを設定し、クラッシュと低速リクエストをシミュレートして、監視データの報告を確認してください。
- チャレンジ (⭐⭐⭐):完全なCI/CDパイプライン(GitHub Actions)を記述してください:analyze → test → release check → build(android + ios + web)→ deploy、ワンクリックマルチプラットフォームリリースを実現。