📌

flutterでスマホアプリ開発するときに必ずやってること

9 min read 1

1. Bundle IDとアプリ名を本番、開発版で分ける

1端末に本番、開発版アプリを同居させたい。また、デバッグビルドの場合は開発版、リリースビルドの場合は本番版としたい。iOS、Androidの話

iOS
xcodeでios/Runner.xcworkspaceを起動

ios/Flutter/Debug.xcconfig
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app.dev
DISPLAY_NAME = Debug StartApp
ios/Flutter/Release.xcconfig
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app
DISPLAY_NAME = StartApp

xcconfigのアプリ名を反映

ios/Runner/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
・・・
<dict>
・・・
  <key>CFBundleDisplayName</key>
  <string>$(DISPLAY_NAME)</string>

TARGETS > Runner > Build SettingsのProduct Bundle IdentifierのRunner列を削除して(deleteボタン)、xcconfigのBundle IDを反映

before

after

Android

本番用signingの準備

git管理対象外とする

.gitignore
# Android Signing
android/app/signing/key.jks
android/app/signing/signing.gradle

JKSファイルをプロジェクトに追加
android/app/signing/key.jks

android/app/signing/signing.gradle
signingConfigs {
    release {
        storeFile file("key.jks")
        storePassword "xxxxx"
        keyAlias "xxxxx"
        keyPassword "xxxxx"
    }
}

Bundle ID、アプリ名と本番ビルド時のsigningを設定

android/app/build.gradle
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
        }
    }

上で設定したアプリ名を使用するようにする

android/app/src/main/AndroidManifest.xml
<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管理対象外とする

.gitignore
# 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管理対象外とする

.gitignore
# Firebase Settings Files
android/app/src/debug/google-services.json
android/app/src/release/google-services.json

gradleプラグインを追加

android/build.gradle
buildscript {
    ・・・
    dependencies {
        classpath 'com.google.gms:google-services:4.3.4'
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
	・・・
    }
}
android/app/build.gradle
・・・
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'

flutterでfirebaseの初期化

lib/main.dart
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

各環境の定数定義

lib/environment.dart
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. ローカライズ

日本語と英語くらいは最低限対応したアプリを作りたい

pubspec.yaml
dependencies:
  ・・・
  flutter_localizations:
    sdk: flutter
lib/localize.dart
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にまとめた

https://github.com/nrikiji/flutter_starter

Discussion

DEBUG・RELEASEを開発・本番に紐づけてしまうとリリースビルドでしか発生しないバグを検証しづらくなってしまうので、開発・本番をわけるのは flutter_flavorizr などで明示的に分けたほうが良いと思います。

ログインするとコメントできます