【Flutter】iOSプロジェクトへのAdd-to-appにおける3つのOptionの比較

リクルートライフスタイルで「じゃらん」のアプリ開発を担当している桐山です。以前、FlutterのUI構築の仕組みについて記事を書きました。今回は既存のiOS/AndroidプロジェクトにFlutterプロジェクトを組み込む仕組みである、Add-to-appについて紹介いたします。

Add-to-app

既存のiOS/AndroidプロジェクトにFlutterを導入したい場合に、プロジェクト全体を書き換えることが容易ではない場合が存在します。その様な場合にFlutterは、Flutterプロジェクトを既存のiOS/Androidプロジェクトに組み込む仕組みを提供しています。その仕組みがAdd-to-appです。

Add-to-appのドキュメントでは、iOSプロジェクトとAndroidプロジェクトへのAdd-to-appの方法が紹介されています。iOSとAndroidのAdd-to-appの方法は異なりますが、我々のチームではまず既存のiOSプロジェクトに対してAdd-to-appを導入したため、以下ではiOSプロジェクトを対象としたAdd-to-appについて言及します。なお、AndroidプロジェクトへのAdd-to-appは今後導入予定で、Androidに関する記事はそのタイミングで公開したいと思います。

iOSプロジェクトへのAdd-to-app

Add-to-appのドキュメントでは、Option A、Option B、Option Cの3つの方法が紹介されています。(2020/4/13 現在)

Option 概要
Option A Cocoapodsを使用してFlutterプロジェクトを組み込む
Option B 必要なFrameworkを生成し、手動でiOSプロジェクトに追加する
Option C Option Bの方法で、Flutter.frameworkをpodspecから取得する

これらの3つのOptionはどの様に使い分けると良いのでしょうか。

ドキュメントにも記載はありますが、この記事では実際に導入してみるとどういう違いがあるのかという点について、内部挙動を踏まえた上でメリットとデメリットを整理します。また、どの様なケースにおいてどのOptionを選択すると良いのかについて、私の考えを述べます。

Option A - Embed with CocoaPods and the Flutter SDK

Option Aは、CocoaPodsを使用してFlutterプロジェクトを組み込む方法です。まずはOption Aでどの様なことが行われるのか見ていきます。 なお、以下で登場するMyAppmy_flutterは、それぞれサンプルのiOSプロジェクト名とサンプルのFlutterプロジェクト名です。

Option Aが行っていること

Option Aの方法では、PodfileにおいてFlutterプロジェクトが保持するpodhelper.rbスクリプトのinstall_all_flutter_podsを呼び出します。

1
2
3
target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end

install_all_flutter_podsはまず、必要なFrameworkをiOSプロジェクトに追加します。追加されるFramewokは以下です。

  • Flutter.framework
  • App.framework
  • FlutterPluginRegistrant.framework
  • 各pluginのframework

なお、この段階で追加されるApp.frameworkはダミーのFrameworkであり、後述するxcode_backend.shで構築されるApp.frameworkと置き換えられます。

install_all_flutter_podsは必要なFrameworkを追加後、次にiOSプロジェクトのBuild PhaseにRun Flutter Build Scriptを追加します。Run Flutter Build Scriptでは、次の様に2つのスクリプトが実行されます。

1
2
3
4
set -e
set -u
source "${SRCROOT}/my_flutter/.ios/Flutter/flutter_export_environment.sh"
"$FLUTTER_ROOT"/packages/flutter_tools/bin/xcode_backend.sh build
  • flutter_export_environment.sh
  • xcode_backend.sh

flutter_export_environment.sh

flutter_export_environment.shは、Flutterによって自動生成されるスクリプトで、次の様に環境変数の設定を行います。

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/Users/kiriyama-keisuke/src/github.com/flutter/flutter"
export "FLUTTER_APPLICATION_PATH=/Users/kiriyama-keisuke/src/ghe.misosiru.io/kiriyama-keisuke/MyApp/my_flutter"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "SYMROOT=${SOURCE_ROOT}/../build/ios"
export "OTHER_LDFLAGS=$(inherited) -framework Flutter"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1"

xcode_backend.sh

xcode_backend.shは、flutter_export_environment.shで設定された環境変数の情報を元に、Flutterプロジェクトのビルドを行います。この時、Flutterプロジェクトのビルドモードは、実行されるiOSプロジェクトのBuild Configuration名、もしくはFLUTTER_BUILD_MODEから決定されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
local artifact_variant="unknown"
case "$build_mode" in
  *release*) build_mode="release"; artifact_variant="ios-release";;
  *profile*) build_mode="profile"; artifact_variant="ios-profile";;
  *debug*) build_mode="debug"; artifact_variant="ios";;
  *)
    EchoError "========================================================================"
    EchoError "ERROR: Unknown FLUTTER_BUILD_MODE: ${build_mode}."
    EchoError "Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)."
    EchoError "This is controlled by the FLUTTER_BUILD_MODE environment variable."
    EchoError "If that is not set, the CONFIGURATION environment variable is used."
    EchoError ""
    EchoError "You can fix this by either adding an appropriately named build"
    EchoError "configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the"
    EchoError ".xcconfig file for the current build configuration (${CONFIGURATION})."
    EchoError "========================================================================"
    exit -1;;
esac
設定 Flutterプロジェクトのビルドモード
Build Configuration名がDebug
もしくは
FLUTTER_BUILD_MODE=debug
debugモード
Build Configuration名がProfile
もしくは
FLUTTER_BUILD_MODE=profile
profileモード
Build Configuration名がRelease
もしくは
FLUTTER_BUILD_MODE=release
releaseモード
それ以外 エラーが発生する

つまり、Option AではiOSプロジェクトがビルドされる際に、Flutterプロジェクトのビルドが都度行われるということです。そしてそのタイミングにおいて、Flutterプロジェクトのビルドモードが決定されます。 これらを踏まえ、Option Aを採用した場合のメリットとデメリットを整理します。

Option Aを採用する場合のメリット

Option Aを採用した場合、常にFlutterプロジェクトの最新の状態で開発を進めることができます。Flutterプロジェクトのコードを変更した場合、iOSプロジェクトを実行するだけでその変更が反映されます。

また、現時点(Flutter v1.12.13+hotfix.9)では、releaseのビルドモードはシミュレータで実行することができないため、環境に応じてFlutterプロジェクトのビルドモードを切り替えられると、開発やリリース作業を行いやすくなります。その点から、上記の様にビルドモードを切り替える処理が予め用意されている点もメリットとして挙げられます。自前でその様な仕組みを構築する必要はなく、またそれぞれのビルドモードのバイナリを予め用意しておく必要もありません。xcode_backend.shにおいて必要な、バイナリを自動で組み込んでくれます。

Option Aを採用する場合のデメリット

Option Aを採用することのデメリットとしてまず挙げられるのが、ビルド時間です。iOSプロジェクトのビルドを行うたびにFlutterプロジェクトのビルドが走るため、その分ビルド時間が長くなってしまいます。

また、Flutterプロジェクトのビルドが行われるが故に、開発者全員にFlutterの開発環境が必要になる点もデメリットとして挙げられます。この点、もしiOSの開発チームとFlutterの開発チームが分かれている様な場合には、iOSの開発チーム全員にもFlutterの開発環境を構築してもらう必要があったり、Flutterのバージョンアップの際にはその作業を行ってもらう必要があります。この点は、コストになってしまいます。

Option Aが適しているケース

メリットとデメリットを踏まえてOption Aが適しているケースとして、iOSプロジェクトとFlutterプロジェクトをセットで開発を進める場合が挙げられます。 例えば、Method Channelを使用してiOSプロジェクトの機能を呼び出さないとFlutterプロジェクトの動作確認ができない様な場合には、iOSプロジェクトにFlutterプロジェクトを組み込んだ状態で開発を進めることになります。その様な場合、Option Aを選択すれば、iOSプロジェクトを実行するたびに、Flutterプロジェクトが最新の状態で組み込まれるため、他のOptionと比べて開発効率が良いと言えます。

Option B - Embed frameworks in Xcode

Option Bで紹介されている方法は、flutter build ios-frameworkコマンドにより必要なframeworkを生成し、iOSプロジェクトに追加する方法です。 flutter build ios-framework --output=some/path/MyApp/Flutter/を実行すると、--outputオプションで指定したディレクトリに必要なframeworkが生成されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.framework
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework 
    ├── Profile/
    │   ├── Flutter.framework
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework
    └── Release/
        ├── Flutter.framework
        ├── App.framework
        ├── FlutterPluginRegistrant.framework
        └── example_plugin.framework

生成されるFrameworkはDebug、Profile、Releaseのビルドモードで生成され、これらをiOSプロジェクトに追加します。

Option Bを採用する場合のメリット

Option Bを採用した場合、ビルド済みのFrameworkを配布するため、iOSプロジェクトのビルド時にFlutterプロジェクトのビルドは走りません。すなわちOption Aの場合に比べてiOSプロジェクトのビルド時間が短く済むということです。 また、開発者全員にFlutterの開発環境を構築をしてもらう必要もなくなります。

Option Bを採用する場合のデメリット

Option Bを採用した場合、iOSプロジェクトをビルドしただけではFlutterプロジェクトのコードに行われた変更は反映されません。Flutterプロジェクトのコードの変更を行った際には、flutter build ios-frameworkコマンドを実行し、最新の状態のframeworkを構築する必要があります。iOSプロジェクトに組み込んだ状態でFlutterプロジェクトの動作確認を頻繁にする様な場合には、この点によって開発効率が低下してしまいます。

Option Bが適しているケース

Option Bが適しているケースとしては、iOSプロジェクトの開発とFlutterプロジェクトの開発を独立して進められる場合が挙げられます。この場合はFlutterプロジェクトの開発が完了し、Add-to-app状態で動作確認をしたいタイミングでFrameworkの生成を行えば十分です。その様なケースでOption Bを採用すれば、iOSプロジェクトのビルド時間は長くならず、またFlutterプロジェクトの開発を行わない開発者にFlutterの開発環境を構築してもらう必要もなくなります。

Option BでFlutterプロジェクトのビルドモードを切り替える

Option Aの方法では、iOSプロジェクトのビルド時にFlutterプロジェクトのビルドを行うため、そのタイミングでFlutterプロジェクトのビルドモードを決定することが可能でした。しかしOption Bを選択した場合は、ビルド済みのFlutterプロジェクトをiOSプロジェクトに組み込むため、その様なことはできません。すなわち予め各ビルドモードのFrameworkをiOSプロジェクトに追加しておく必要があるということです。

この点に関しては次の様な方法を用いることで、iOSプロジェクトのBuild Configurationに紐づけて、使用するFrameworkを切り替えることができます。

まず、debugのビルドモード用のpodspec(my_flutter_debug.podspec)と、releaseのビルドモード用のpodspec(my_flutter_release.podspec)を用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
some/path/MyApp/
└── Flutter/
    ├── my_flutter_debug.podspec
    ├── my_flutter_release.podspec
    ├── Debug/
    │   ├── Flutter.framework
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework 
    │   └── example_plugin.framework
    ├── Profile/
    │   ├── Flutter.framework
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework
    └── Release/
        ├── Flutter.framework
        ├── App.framework
        ├── FlutterPluginRegistrant.framework
        └── example_plugin.framework
  • my_flutter_debug.podspec
1
2
3
4
5
6
7
8
9
10
11
12
13
Pod::Spec.new do |spec|
  spec.name         = "my_flutter_debug"
  spec.version      = "0.0.1"
  spec.summary      = "A short description of my_flutter."
  spec.description  = "A long description of my_flutter."
  spec.homepage     = "http://EXAMPLE/my_flutter"
  spec.license      = "MIT"
  spec.author       = { "kiriyama-keisuke" => "hogehoge" }
  spec.source       = { :path => '.' }
  spec.vendored_frameworks = 'Debug/Flutter.framework', 'Debug/App.framework', 'Debug/FlutterPluginRegistrant.framework', 'Debug/url_launcher.framework'
end
  • my_flutter_release.podspec
1
2
3
4
5
6
7
8
9
10
11
12
13
Pod::Spec.new do |spec|
  spec.name         = "my_flutter_release"
  spec.version      = "0.0.1"
  spec.summary      = "A short description of my_flutter."
  spec.description  = "A long description of my_flutter."
  spec.homepage     = "http://EXAMPLE/my_flutter"
  spec.license      = "MIT"
  spec.author       = { "kiriyama-keisuke" => "hogehoge" }
  spec.source       = { :path => '.' }
  spec.vendored_frameworks = 'Release/Flutter.framework', 'Release/App.framework', 'Release/FlutterPluginRegistrant.framework', 'Release/url_launcher.framework'
end

そして、iOSプロジェクトのPodfileにおいて、次の様に:configurationを指定します。

1
2
pod 'my_flutter_debug', :path => './Flutter', :configuration => 'Debug'
pod 'my_flutter_release', :path => './Flutter', :configuration => 'Release'

そしてpod installを実行し各Frameworkを追加します。 この様に設定することで、DebugのBuild Configurationが使用される場合には、debugのビルドモードのFrameworkが使用され、ReleaseのBuild Configurationが使用される場合には、releaseのビルドモードのFrameworkが使用されます。なお、次の様に複数のBuild Configurationに紐づけることも可能です。

1
pod 'my_flutter_release', :path => './Flutter', :configurations => ['Release', 'AdHoc']

参考: Link with different Pods according to the build configuration

Option C - Embed application and plugin frameworks in Xcode and Flutter framework with CocoaPods

Option Cは、Flutterのv1.13.6から使用可能です。 Option Bで紹介されているflutter build ios-frameworkコマンドに--cocoapodsオプションを付与することで、Flutter.frameworkではなくFlutter.podspecが生成されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.podspec
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework (each plugin with iOS platform code is a separate framework)
    ├── Profile/
    │   ├── Flutter.podspec
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework
    └── Release/
        ├── Flutter.podspec
        ├── App.framework
        ├── FlutterPluginRegistrant.framework
        └── example_plugin.framework

Option Cを採用する場合のメリット

Option Cを採用することのメリットは、基本的にOption Bと同じです。 異なるのは、Flutter.podspecが生成されるため、サイズの大きいFlutter.frameworkを配布する必要がなくなるという点です。

Option Cを採用する場合のデメリット

Option Cを採用することのデメリットも、基本的にはOption Bと同じですが、 Option Cの方法では、Podの名前が衝突してしまうため、Option Bで紹介した:configurationの指定によって使用するFrameworkのビルドモードを切り替えることはできません。 すなわち、以下の様な指定をすることはできないということです。

1
2
pod 'Flutter', :path => './Flutter/Debug', :configuration => 'Debug'
pod 'Flutter', :path => './Flutter/Release', :configuration => 'Release'
1
2
3
4
[!] There are multiple dependencies with different sources for `Flutter` in `Podfile`:
- Flutter (from `./Flutter/Debug`)
- Flutter (from `./Flutter/Release`

Option CでFlutterプロジェクトのビルドモードを切り替える

Build Configurationによって使用するFlutter.frameworkを切り替えることはできないものの、Podfileに以下の様に指定すれば環境変数で使用するFlutter.frameworkを切り替えることができます。

1
2
3
4
5
6
7
def path_for_debug_or_release
  if ENV['POD_RELEASE'] =~ "true"
    './Flutter/Release'
  else
    './Flutter/Debug'
  end
end
1
pod 'Flutter', :podspec => path_for_debug_or_release

参考: Allow different version of a pod per build configuration

Option Cが適しているケース

Option Bと同様に、iOSプロジェクトの開発とFlutterプロジェクトの開発を独立して進められる場合で、尚且つFlutter v1.13.6以降を使用しているのであれば、Option Cが最も有用な選択肢と考えられます。Option Bのメリットに加え、サイズの大きいFlutter.frameworkを配布する必要がなくなります。

まとめ

iOSプロジェクトへのAdd-to-appの3つのOptionについて、それぞれのメリットとデメリットを整理し、適しているケースについて言及しました。まとめると以下になります。

Option メリット デメリット
A - 常にFlutterプロジェクトの最新の状態で開発を進められる
- ビルドモードの切り替えの処理が予め用意されている
- iOSプロジェクトのビルド時間が長くなる
- 開発者全員にFlutterの開発環境が必要
B - iOSプロジェクトのビルド時間が長くならない
- Flutterプロジェクトの開発を行わない開発者は、Flutterの開発環境が不要
- Flutterプロジェクトの最新の状態をiOSプロジェクトに組み込む際、都度コマンドの実行が必要になる
C - Option Bのメリットに加え、サイズの大きいFlutter.frameworkの配布が不要 - Option Bのデメリットに加え、Build Configurationに紐づけてFlutter.frameworkのビルドモードの切り替えができない
Option 適しているケース
A iOSプロジェクトとFlutterプロジェクトをセットで開発を進める場合
B iOSプロジェクトの開発とFlutterプロジェクトの開発を別々に進められる場合
C iOSプロジェクトの開発とFlutterプロジェクトの開発を別々に進められるかつFlutter v1.13.6以降を使用している場合

なお、上記の適しているケースについては筆者の一意見であり、それぞれのプロジェクトの状態から適切に判断する必要がある点はご留意ください。 この記事を参考に、Flutterがより多くのプロジェクトに導入されると嬉しいです。 最後まで読んでいただきありがとうございました。