Android
iOS
Flutter

既存のAndroid/iOSアプリからFlutterを呼び出す(Add-to-App project)

Flutter Meetup Tokyo #8の登壇内容をまとめました。

発表資料はこちらです。

ちなみに、登壇当時はAdd2Appという表記でしたが、現在はAdd-to-Appとなっていますので、本記事ではApp-to-Appで統一します。

サンプルコードもこちらにおいておきました。

https://github.com/shcahill/Add2AppSample


App-to-App projectとは?

公式ページはこちらです。

これによると、


  • 既存のAndroid/iOSのプロジェクトから簡単にFlutterを呼び出すことができる

  • 現状、In previewの機能であり、master channelからしか利用できない(2019/3/17時点)

となっています。

Flutterの利用シーンが現状、新規アプリ作成時に限定されているため、開発者がFlutterに触れる機会も同時に限定されています。その対策としてAdd-to-App projectが進められています。React Nativeでは既に存在する機能でもあり、今後Flutterが普及するかどうかのひとつの指標になるかもしれません。

現在の進捗状況に関しては、こちらから確認できます。

https://github.com/flutter/flutter/projects/28

また、ひとつ注意点があります。master channelでしか利用できないと述べましたが、その場合安定していないFlutterバージョンを使用することになります。当然、不具合も混入している可能性も高くなります。ですので、最低限既知の不具合を下記のBad Buildsから確認の上、利用することをおすすめします。

https://github.com/flutter/flutter/wiki/Bad-Builds


Add-to-Appの基本的な仕組み

スクリーンショット 2019-04-20 0.10.03.png

一般のFlutterプロジェクトでは、Dartコードのあるlibパッケージと同じ階層にandroidパッケージとiosパッケージが同居しています。

一方App-to-Appでは、Android/iOSプロジェクトと同列の階層に、Flutterモジュールを用意して使います。Androidからはgradle経由でモジュールとして取り込み、iOSではcocoapodsを使って参照します。


実装方法


Flutter moduleの作成

master channelに切り替えます。

※channelを切り替えたあと、他のプロジェクトをビルドする際は元に戻すことを忘れないようにしておきましょう

$ flutter channel master

次にFlutterモジュールを生成します

$ flutter create -t module モジュール名

これでFlutter module project templateが作成されます。

スクリーンショット 2019-04-20 0.10.49.png

生成されたモジュールには、隠しパッケージとして.android.iosパッケージが生成されています。これがネイティブとDartとのブリッジとして動作します。


各OSの設定

次は先ほど作ったモジュールをAndroid/iOSから参照する設定を行います。


Androidプロジェクトの設定

CompileOptionでJava1.8の指定を行います。


build.gradle

android {

compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}

次にFlutterモジュールへのpathを設定します。


setting.gradle

include ':app'

// add below
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'モジュール名/.android/include_flutter.groovy'))

最後におなじみのimplementationです。


app/build.gradle

dependencies {

implementation project(':flutter')
...
}


[optional]AndroidX対応

既存のプロジェクトがAndroidXに対応している場合はもう一手間必要です。既存のテンプレートモジュールではAndroidXに対応していないため、手動でモジュール内の.androidをAndroidX対応させる必要があります。といっても、importを変えてあげるくらいなので、さほど手間ではないと思います。


iOSプロジェクトの設定

cocoapodsを使用するため、Podfileを編集します。(Podfileがない場合はpod initを実行してください)


Podfile

flutter_application_path = '../モジュール名/'

eval(File.read(
File.join(flutter_application_path,
'ios', 'Flutter', 'podhelper.rb')),
binding)

Podfileを生成したらpod installを実行してください。

次にプロジェクト設定を行います。

Flutterは現状、bitcodeに対応していないため、Enable bitcodeNoにしてください。

スクリーンショット 2019-04-19 23.17.27.png

最後にbuild phaseにDartのビルドスクリプトを組み込みます。

スクリーンショット 2019-04-19 23.18.25.png

Build PhasesからNew Run Script Phaseを行い、下記のScriptを記述してください。


RunScript

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

上記スクリプトを記述したら、一番上のTarget Dependenciesのすぐ下にドラッグで移動させてください。

スクリーンショット 2019-04-19 23.23.03.png


各OSでの呼び出し

ここまでで、まずビルドは通るようになっているはずです。

ここからは既存のプロジェクトからの呼び出し方について説明します。


Androidからの呼び出し

AndroidからはFragmentとして呼び出す方法と、Viewとして呼び出す方法があります。が、どちらも使い方にあまり違いはありません。


Viewとして呼び出す


MainActivity.kt

val flutterView = Flutter.createView(

this, // Activity
lifecycle, // Lifecycle
"route1")

このコードを実行すると、Android画面内におなじみのFlutterの画面がViewとして表示されます。

スクリーンショット 2019-04-19 23.26.43.png

第三引数の"route1"については、後述します。


Fragmentとして呼び出す


MainActivity.kt

val transaction = supportFragmentManager.beginTransaction()

transaction.replace(R.id.container,
Flutter.createFragment("route2"))
transaction.commit()

Viewの場合とほぼ同じであることがわかります。

ここで先ほども出てきた"route"について触れておきます。


What is "route"?

ネイティブからFlutterを呼び出した際、Dartでは以下のコードが実行されます。


main.dart

void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return SomeWidget();
case 'route2':
return SomeWidget();
default:
return Center(child: Text('Unknown route: $route'));
}
}


このようにDart側では、ネイティブ側から引数で渡した文字列が受け取ることができ、その文字列を元に表示するWidgetを振り分けることができるようになっています。


iOSからの呼び出し

Androidと異なり、少々前準備が必要です。


FlutterAppDelegateを継承

まずAppDelegate.swiftFlutterAppDelegateを継承させます。


AppDelegate.swift

import Flutter

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
}


なお、FlutterAppDelegateは以下のような定義になっています。


FlutterAppDelegate.swift

@interface FlutterAppDelegate: UIResponder<UIApplicationDelegate, ...>


また、pluginを利用する場合はもう少し実装が必要になります。


AppDelegate.swift

import Flutter

import FlutterPluginRegistrant

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
var flutterEngine: FlutterEngine?

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.flutterEngine = FlutterEngine(name: "io.flutter", project: nil)
self.flutterEngine?.run(withEntrypoint: nil)
GeneratedPluginRegistrant.register(with: self.flutterEngine)
return super.application(application, didFinishLaunchingWithOptions: launchOptions); |
}
}



ViewControllerから呼ぶ

呼び方はシンプルです。


ViewController.swift

let flutterViewController = FlutterViewController()

flutterViewController.setInitialRoute("route3")
present(flutterViewController, animated: false, completion: nil)

基本的な使い方は以上です。


Tips


Flutterからネイティブ画面に戻る


main.dart

SystemNavigator.pop();



Hot Reload

App-to-App projectに対する、IDEとしてのHot Reloadサポートは、まだIn progressの状態です。が、コマンドラインツールは既に提供されています。

$ cd flutterモジュールのパス

$ flutter attach

コマンドを実行すると、以下のような画面になります。

スクリーンショット 2019-04-20 0.35.24.png

Hot Reloadにはpress "r"、Hot Restartにはpress "R"とあります。

また、http://127.0.0.1....とURLがひとつありますが、これをブラウザで開くとデバッグツールがみれます。

スクリーンショット 2019-04-20 0.37.45.png


Method Channel

今回の話とは逆のパターンで、こちらの方が利用シーンは圧倒的に多いと思います。Qiitaの記事を書いていますので、ご参考までにどうぞ。

FlutterでAndroid/iOSのネイティブ画面を表示する

公式ページはこちら

https://flutter.dev/docs/development/platform-integration/platform-channels