Flutterで環境ごとにビルド設定を切り替える — iOS編
モバイルアプリ開発において、環境ごとに設定を変えてビルド・配信することはほぼ必須と言えます。最低限、以下くらいには分けると円滑に開発・テスト・リリースができます。
- 開発環境版
- 社内テスト配布版(個人開発なら無くとも何とかなる)
- リリース版
さらにCIサービスを使うなどして諸々自動化できるとなお良いです。本記事ではこれをFlutterではどう扱えば良いかを解説していきます。
サンプルもこちらに置いてあります:
公式ドキュメントの不在
まず、公式ドキュメントが充実しているFlutterには珍しく、そこにはFlavorの説明がありません。
また、 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は無指定で良いです(既存の Profile
と Release
の: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で初期化しようとするからです。
ちなみに、起動直後に呼ばれるこのコード経由で、
各プラグインの以下のコードが呼ばれています。
今回は環境ごとに違うものを使いたいので、明示的にどのファイルを指定するのかをそれらが実行される前に呼べば解決します。
あるいは、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 asFIRTimestamp
objects instead ofNSDate
objects. Update any code that expects to receive aNSDate
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での実践的なiOSアプリ開発をスムーズに進めるための足がかりになるのではと思います。
以下の記事では、本記事のサンプルを元にさらに、昨年2018年12月に開催された Flutter Live event で発表された Flutter用の無料CIサービスであるCodemagicにてCIを構築する過程を解説していきます。
その他参考になる記事
本文中では触れませんでしたが、次の記事も良かったです。