Flutterで環境ごとにビルド設定を切り替える — iOS編

mono 
mono 
Jan 7, 2019 · 31 min read

モバイルアプリ開発において、環境ごとに設定を変えてビルド・配信することはほぼ必須と言えます。最低限、以下くらいには分けると円滑に開発・テスト・リリースができます。

  • 開発環境版
  • 社内テスト配布版(個人開発なら無くとも何とかなる)
  • リリース版

さらにCIサービスを使うなどして諸々自動化できるとなお良いです。本記事ではこれをFlutterではどう扱えば良いかを解説していきます。

サンプルもこちらに置いてあります:

公式ドキュメントの不在

まず、公式ドキュメントが充実しているFlutterには珍しく、そこにはFlavorの説明がありません。

Creating flavors for Flutter

また、 flutter run --help で確認しても次のような簡素なヘルプが出るだけです。

というわけで、上の公式ドキュメントにリンクが貼られている以下の2つの記事がまず一番信頼できそうなので、それを参考にしつつ実際に手を動かしてみました。

その他、”flutter flavor”(+知りたいキーワード)でググったりIssue検索するとそれなりに情報は出てきます。

なお、Flavorに関すること以外は公式ドキュメントでもしっかり抑えられていて、これらも十分参考になります。

実際の手順に移る前に概念をざっと説明します。

Flutterのビルドモード

まず、Flutterのビルドモードには、以下の3種類があります。

  • Debug: 普段の開発(JITコンパイルゆえにHot Reloadが効く代わりにパフォーマンス劣化)
  • Release: リリース時(AOTコンパイルゆえにHot Reloadは効かない代わりにパフォーマンスが良い)
  • Profile: プロファイリングしたい時(本記事とは関係しない)

Profileはここでは無視すると、DebugとReleaseモードの2つがあることになります。Flutterの場合このモードの違いによってHot Reloadの有無があったりパフォーマンスの差がより顕著だったりしますが、他のプラットフォームでのアプリ開発にもあるDebugとReleaseビルドの区別と役割的にはほぼ同等です。

また、Flutterのビルドモードとネイティブアプリ側のビルドモードは基本的には合わせるべきです。なので、Flutterのビルドモード: Debug, ネイティブのビルドモード: Release、のようなチグハグな組み合わせはなく、以下の2通りに収まるはずです。

  • Flutter・ネイティブ側ともにReleaseビルド
  • Flutter・ネイティブ側ともにDebugビルド

つまり、ビルドモードについてはネイティブ側の設定をFlutterのビルドモードに合わせるのが肝要です。

環境のバリエーション

基本的に、以下の3通りに大きく分けられます(運用次第でもっと細かく分けることもあります)。

  • 開発環境
  • テスト環境
  • 本番環境

それぞれ接続サーバー先が違ったりFirebaseを使っている場合プロジェクトが変わったりします。

英語としてどう表すかはマチマチですが、この記事ではそれぞれ以下のように表すことにします。

  • Development
  • Staging
  • Production

というわけで実現したいこととしては、以下の直交する組み合わせを設定して、

  • ビルドモード: 2通り
  • 環境のバリエーション: 3通りくらい

以下を実現したいということです。

  • アプリの外観(アプリ名・アイコンなど)を変えられるように
  • Flutterおよびネイティブ側で今の状態を取得してアプリの振る舞いを変えられるように

Flutterでは環境のバリエーションはどう表現するのか

flutter build および flutter run コマンドの以下のオプションで行います。

  • --flavor でネイティブ側に伝える
  • --target(-t) でFlutterのエントリーポイントファイルを指定(デフォルトは lib/main.dart )して、それによって振る舞いを変える

どちらか片方だけ指定して、platform channelsで伝えるという別解もありえそうですが、処理が煩雑になりそうですし何より環境情報を早めのタイミング(ビルド前など)で知らせないとできないこともあるのでこのようにコマンドのオプションとして外部から与える方が良いです。

flavorについてはここではざっくり理解にとどめて、あとで具体的に説明します。

iOSアプリの設定方法

それでは、iOSアプリの設定方法を説明していきます。Androidの方が簡単で、次の記事で充分に思いますが、自分なりに書きたい事項があれば別途記事を書くかもしれません。

サンプルではAndroid対応もされていて、対応コミットはこちらです。

一方、iOSはネイティブ開発に慣れていてもかなり難しかったです。元々ややこしいこととFlutterはAndroid対応優先していることの2点に起因している気がします。

というわけで、説明していきます。

新規プロジェクト作成

新規プロジェクト作成直後、 ios/Runner.xcworkspace を開くと次のように、上述のFlutterの3通りのビルドモードに相当するConfigurationsがすでに設定されています。

このため、デフォルトのConfigurationでも次のような使い分けくらいは可能です。

  • Debug: 開発環境
  • Release: リリース環境

さらに、次のようにProduct Bundle Identifierを変えると別アプリ扱いにできます。

個人開発ならこれでもギリギリ耐えうるかもしれませんが、色々考慮するとやはり他の組み合わせも欲しくなってきます。具体的には以下などです。

  • 諸々の設定は開発環境を踏襲しつつFlutterのReleaseモードでビルド(手元でのビルドで実際のパフォーマンスをチェックしたい時など)
  • FlutterのReleaseモードでビルドしてStagingサーバーに繋がるものを社内配布

というわけで、本記事では以下のバリエーションを用意します。どういうバリエーションを用意するべきかはプロジェクトの要件次第で変わります。

カスタムSchemeを用意

まず初期状態で以下の実行を試してみると、

flutter run --debug --flavor development

次のようにエラーが発生します。

flavorに対応するカスタムSchemeが必要とのことなので、次のメニューから編集画面を開きます。

そして、RunnerをベースTargetとして以下のように使いしたいFlavorバリエーションを追加します。通常チーム共有するはずなので、Sharedにもチェックを入れておきます。

ここでまた実行して試してみると、次のようなエラーが表示されます。

Configurationを追加

つまり、 [ビルドモード]-[Flavor] のConfigurationが必要とのことです。今回は次のConfigurationが必要です。

Debug- 系は元々あったDebugから、 Release- 系は元々あったReleaseからDuplicateするのが大事です。これでネイティブ側でデフォルトで適切に設定されているビルド設定が引き継がれます。

デフォルトのDebugを消したら、Android StudioからXcodeを開くときにエラーになってしまったので残しておきました(ついでにReleaseも)。それぞれConfigurationには一応以下を設定しています。

  • Debug: Debug-Development
  • Release: Release-Production

先ほどの表にConfiguration名を添えるとこうなります。

これでとりあえず次のコマンドでの起動が成功するはずです。

flutter run --debug --flavor development

Android Studio経由の場合、次のように設定します。

xcconfigを設定

上記手順までではFlavorの区別による振る舞いの違いが起こらないので、次のようにConfigurationごとにxcconfigを設定します。

xcconfigについては以下などご参照ください。見つけるのがとても難しいですが公式ヘルプも存在します。

デフォルトでビルドモードの区別のために次のものが存在しますが、

  • Debug.xcconfig
  • Release.xcconfig

Flavorに応じて以下を追加します。

  • Development.xcconfig
  • Staging.xcconfig
  • Production.xcconfig

さらにそれらを#includeした以下などを必要な組み合わせに応じて用意して、Configurationに設定します。

  • Debug-Development.xcconfig
#include "Debug.xcconfig"
#include "Development.xcconfig"

詳しくは、サンプルを見た方が良いと思いますが、ざっと説明します。

Debug.xcconfigには以下を追加して、プログラム中で #if DEBUG による判定をできるようにしておきます。

OTHER_SWIFT_FLAGS = $(inherited) "-D" "DEBUG"

一方、Release.xcconfigには以下を追加しておきます。

TRACK_WIDGET_CREATION=

これを指定しておかないとアーカイブ時にエラーが発生します。

環境ごとに、FLUTTER_FLAVOR=Developmentなどそれぞれ指定すると、プログラムからFlavorを判定できるようになります。

  • Development.xcconfig
  • Staging.xcconfig
  • Production.xcconfig

諸々設定すると、プロジェクト設定が次のような状態になります。

Podfileを編集してinstall

ここですごくハマったのですが、(デバッグ系の)Configurationが増えたのにPodfileをそのまま使っているとXcodeからシミュレーターで実行しようとすると次のようなビルドエラーが発生します。
(プロジェクト状態によっては運良く発生しないこともありますが、メソッドチャンネルなど使ったライブラリーに依存していると発生するはずです。)

Podfileに対して、追加したConfigurationに対して次のような変更が必要で、変更後 pod install すると上のエラーは解消するはずです。

project 'Runner', {
'Debug' => :debug,
'Debug-Development' => :debug, #追加
'Profile' => :release,
'Release' => :release
}

以下のドキュメントにある通り、無指定の場合は :release になるのでRelease系のConfigurationは無指定で良いです(既存の ProfileRelease:release指定も冗長ですが分かりやすさのために明示するのも良いと思います)。

また、Release系のConfigurationをシミュレーターで実行しようとしても同様のエラーが表示されますが、Flutterのリリースモードはシミュレーターでは実行できない仕様なのでReleaseビルドの時は実機を使うようにしましょう。
(大半はHot Reloadの効くDebugビルドで開発するはずで、たまにパフォーマンス具合を実機で確認するときにReleaseビルドする程度なはずなので、この制限は実質不都合無いはずです。)

別アプリ扱いにする

別アプリ扱いにするには、ConfigurationごとにPRODUCT_BUNDLE_IDENTIFIERを変えれば良いのですが、厄介なことに、これが異なるとビルドはできても起動しなくなるという問題が存在します。詳しくは次を見てください。

簡単に言うと、flutter run コマンドにてFlavorは正しく伝わるものの、アプリの起動処理ではproject.pbxprojで設定されているものから先頭のPRODUCT_BUNDLE_IDENTIFIERを取得してしまい、それらが不合致の時起動に失敗します。起動したいものとその偶々先頭で見つかったものとが運よく同じPRODUCT_BUNDLE_IDENTIFIERの場合は成功します。

ただ、この問題の回避策を見つけまして、PRODUCT_BUNDLE_IDENTIFIERがない場合はこの処理に入ってFlavorに応じた正しいPRODUCT_BUNDLE_IDENTIFIERを取得しようとするみたいです。

つまりプロジェクトファイルでは無指定にして、xcconfigのみで設定すればうまく動きました。

例えば、Development.xcconfigに次のように記述します。

PRODUCT_BUNDLE_IDENTIFIER=com.mono0926.flavor.development

プロジェクト設定で、次のように元々文字列が設定されているところにフォーカスを合わせてバックスペースを押すと、

次のようにxcconfigの値がセットされます。

次のようにするとうまくいかないので気をつけてください。

  • テキストフィールドにフォーカスを合わせてから文字列を消すと空文字扱いになってしまう
  • $(inherited) を指定するというのもプロジェクト設定の扱い的には正しいが、Flutterから見るとその文字列のPRODUCT_BUNDLE_IDENTIFIER扱いになってしまう

また、プロジェクトファイルに直接指定だと、アプリIDにハイフンが含まれていてもアプリ起動に失敗する問題がありますが、xcconfig経由だとこれも問題ないです。

[追記] ビルド時に問題になることがある

flutter run までは以上の方法でうまくいったものの、 flutter build では同様の原因でIDの不合致が発生してしまいビルド失敗の原因になってしまうことがあることに気付きました。その場合、ビルドスクリプトの前に以下などを挟むと解消します。ビルド環境によってはこの対処無くとも問題ない気はします。

/usr/bin/plutil -replace CFBundleIdentifier -string com.mono0926.flavor.staging ios/Runner/Info.plist

ついでにアプリ名は次のようにDevelopment.xcconfigなどに記述してinfo.plist にそれを指定すると変えられます。

DISPLAY_NAME=Flavor Dev

その他、上記手順の応用で様々なビルド設定を環境に応じて変えられるはずです。

また、アプリアイコンについては flutter_launcher_icons というツールを使うと便利なので別途記事を書いておきました。

iOSネイティブコード側でビルドモード・Flavorを取得

上でデバッグ系のxcconfigのOTHER_SWIFT_FLAGSにDEBUGを指定したので、次のように判定できます。

Flavorについては、info.plistに指定した上で、

次のように取得できます。
(apiUrlはflavorによって接続情報を変えるイメージを表す例です。)

FlavorとPRODUCT_BUNDLE_IDENTIFIERが一対一の関係になっている場合は別途設定せずPRODUCT_BUNDLE_IDENTIFIERから判定でも良いですが、何か適当なカスタム値をFlavorとして設定して取得する例を兼ねてこう書きました。

AppDelegateあたりに次のようなコード書くと、正しく判定されていることが分かります。

let buildMode = BuildMode.current
let flavor = Flavor.current
print("buildMode: \(buildMode), flavor: \(flavor)")

Firebaseを環境ごとに分ける

Firebaseはアプリ開発においてかなり定番になってきており、特に新規開発やFlutterではとても多く使われていると感じています。というわけで、Firebaseを環境ごとに分ける方法も説明していきます。

セットアップ

通常のセットアップ手順はこのドキュメントにしっかり記載されています。

以下が公式でサポートされているプラグインで一部機能が足りないものもありますが、基本的に網羅されています。

本記事のサンプルでは以下をインストールしました。

複数プロジェクトの用意

Firebaseプロジェクトには複数アプリを紐づけられますが、環境を分ける場合はFirebaseプロジェクトもその環境の数用意するのが普通です。
(今回のサンプルでは手間なので1プロジェクトに複数アプリを紐づけてしまいましたが、要件的にそれが適していると自信を持って言えない限りはお勧めしません。)

プロジェクトを複数作ったら、それぞれに1つずつiOSアプリを紐づけていって、GoogleService-Info.plistという接続情報をダウンロードして、このように環境ごとに名前を変えてRunner配下に置きます。

初期化処理を調整

このまま実行するとエラーが発生します。なぜなら、デフォルトではGoogleService-Info.plistで初期化しようとするからです。

ちなみに、起動直後に呼ばれるこのコード経由で、

各プラグインの以下のコードが呼ばれています。

プラグイン内のFIRAppの初期化コード

今回は環境ごとに違うものを使いたいので、明示的にどのファイルを指定するのかをそれらが実行される前に呼べば解決します。

あるいは、Build PhaseのScriptでGoogleService-Info.plistを差し替えるという別解もあります。個人的には上記のコードで設定するやり方が分かりやすくて好きですが、その別解をやりたい場合は Flutterで本番/ステージング/開発を切り替える — Qiita に書いてあるのでご参照ください。

Firestoreの設定変更

[2018/01/24 追記]

この項で施すFirestoreの設定変更はiOSではSDK 5.16.0からAndroid SDKではCloud Firestore version 18.0.0からデフォルトになったので、不要になりました。ただ、今後何かしら同様の設定を変えたくなった時の参考にもなるので残しておきます。

Breaking change: The areTimestampsInSnapshotsEnabled setting is now enabled by default. Timestamp fields that read from aFIRDocumentSnapshot are now returned as FIRTimestamp objects instead of NSDate objects. Update any code that expects to receive a NSDate object. See the reference documentation for more details.

https://firebase.google.com/support/release-notes/ios#5.16.0

— 追記終わり —

ちなみに、デフォルトでFirestoreを使おうとすると次のようなログが表示されます。

書いてある通り、組み込みのDate型ではなくFirebaseのTimestamp型を使うのが今後の推奨で、少なくとも新規プロジェクトならこれに従って設定を更新するのが良いです。

指示に従って以下のようにiOS側で設定しても良いですが、

Firestore側で以下の処理を、他のFirestoreメソッドを触る前に実行しておくのがオススメです。

Firestore.instance.settings(timestampsInSnapshotsEnabled: true);

ネイティブ側で書く場合、iOSとAndroidで2箇所の記述が必要になりますが、Flutter側で記述しておくと1箇所で済みます。


以上でiOSが絡むものは一通りできたので、Flutter側に移ります。

Flutter側でのビルドモードの判定方法

次の通り、Flutter側でのビルドモードの判定はまだAPIとしては提供されていません。

ただ、自前で次のコードを用意するだけで判定できるようになります。

次のコードを実行すれば正しい値が出力されるはずです。

print('buildMode: $buildMode');

assertを使ってデバッグモードか判定していますが、これはこのDEBUGバナーを表示するかどうかの判定にも使われています。

Flutterの振る舞いをFlavorによって変えたい

上でも軽く触れましたが、これは直接flavorを使うことはできず、--target(-t) でFlutterのエントリーポイントファイルを指定(デフォルトは lib/main.dart )してそれによって振る舞いを変えることになります。

そのため、コマンドを間違えるとFlutterとネイティブでFlavorがチグハグになってしまうので気を付けましょう。

エントリーポイントの振り分け方としては、まず次のようにFlavorを下位ツリーに渡せるWidgetを用意します。
(この例ではどのFlavorかの情報しか持っていないですが、実際には接続情報などもセットで持たせたりすると思います。)

そして、 main_development.dart などFlavorの数だけmainファイルを用意します。

こうすると、下位ツリーで以下のようにしてFlavorを取得できます。

final flavor = FlavorProvider.of(context);

今回用意したビルドモードとFlavorの組み合わせに対応するコマンドは以下のようになります。

iOS側のxcconfigの仕上げ

エントリーポイントファイルを増やしたことに伴い、iOS側のxcconfigに1つ変更が必要です。

Generated.xcconfigの FLUTTER_TARGET を確認すると、 lib/main_development.dart などFlutter側で flutter run で最後に実行したFlavorになっています(デフォルトは lib/main.dart )。

この状態でXcodeから実行すると、Flutter側のエントリーポイントファイルが常にそれになってしまうので、このままでは例えばStaging Flavorで実行してもDeveloploment Flavorとなってしまいます。

これは次のようにFlavorのxcconfigで適切なものに上書きすると解決します。

# Development.xcconfig
FLUTTER_TARGET=lib/main_development.dart
# Staging.xcconfig
FLUTTER_TARGET=lib/main_staging.dart
# Production.xcconfig
FLUTTER_TARGET=lib/main_production.dart

このように設定しておくと、Flutter側・Xcode側の好きな方から実行しても常に同じアプリ状態を保てます。

バージョン・ビルド番号の管理は?

やり方が2通りあります。

pubspec.yamlに頼る

pubspec.yaml のversionを元に、flutter build コマンド実行時にGenerated.xcconfigのFLUTTER_BUILD_NAME・FLUTTER_BUILD_NUMBERが更新されます。

name: flavor_example
description: A Flavor Example.
version: 1.2.3+5

さらに、 それがデフォルトで次のように設定されるようになっているので、特に何もせずpubspec.yaml のversion管理だけに集中すればOKです。

buildコマンドで指定

pubspec.yaml のversionに頼らないやり方もあり、ビルドコマンドの引数でそれぞれ指定すると、pubspec.yaml のversionよりもそれが優先されてGenerated.xcconfigに反映されます(ただしAndroidだと pubspec.yaml+ にてビルド番号が指定されていると --build-number の上書きが効かなかったのでコマンドで指定する場合はそれを消しておいた方が良いと思います)。

flutter build ios --build-name=1.2.3 --build-number=5

詳細は以下のhelp結果を見てください:

ハイブリッド方式

個人的にはpubspec.yaml 指定とビルドオプション指定を組み合わせるのが良いかなという気がしています。

pubspec.yaml のversionではビルド番号の管理をせずにバージョン番号だけを管理・コミットします。

name: flavor_example
description: A Flavor Example.
version: 1.2.3

ビルドコマンドでは — build-numberオプションのみを指定して、CIなどからインクリメントしていく値を設定します。

flutter build ios --build-number=$BUILD_NUMBER

本記事で述べてきたオプションをまとめたビルドコマンドを組むと、次のようになります(Release-Stagingの例)。

実行結果の確認

サンプルには結果が分かりやすく確認できる画面を入れておきました。例えばRelease-Stagingの実行結果は次のようになります。左の下段は、package_info のパッケージにてネイティブ側から取得した値を表示しています。

ちなみに、左の表のレイアウトには、Table Widgetを使いましたが、簡単に組めて良かったです。

左の下のinfoマークをタップすると真ん中のダイアログが表示されますが、これは AboutListTile を使っています。実際のアプリに詳細情報を載せるときに便利ですし、ライセンス一覧表示も対応していて素晴らしいです。


ちなみに、Flutterのアプリ情報は次のPull Requestでかなり楽に取れるようになりそうでしたが、残念ながらマージされずクローズされてしまいました。ビルドシステム刷新後に同様の対応がなされるようです。


Flutter 🇯🇵

Flutterに関する日本語記事を書いていきます🇯🇵

mono 

Written by

mono 

Software Engineer(iOS, Swift, Flutter, Firebase, GCP etc.) / Works at KDDI DIGITAL GATE / https://mono0926.com/page/about/

Flutter 🇯🇵

Flutterに関する日本語記事を書いていきます🇯🇵

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade