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


はじめに

この記事は Flutter #2 Advent Calendar 2018 18日目の記事です。

Flutterを導入しようと思ったときに、ネイティブで画面を作れる方法を知っていると、精神的にも余裕が生まれてFlutter導入へのハードルも低くなるのかなと勝手に解釈してこの記事を書こうと思いました。

なお、タイトルおよびサンプルではネイティブの画面を表示することを目的としていますが、実際は画面を表示せずにAndroid/iOSのコードだけを呼んでOSのAPIを叩くだけなどの実装も可能です。


概要

MethodChannelという機能を使って、Dart,iOS,Androidそれぞれの実装を行います。

公式に載っている構成はこちらです。

Architecture

このようにDartからはMethodChannelでメッセージを送信し、AndroidではMethodChannel、iOSではFlutterMethodChannel経由でメッセージを受け取ります。

ちなみに、AndroidやiOSのコードは、Flutterのプロジェクトからでは修正できません。(できるのですが、補完などができません)

そのため、AndroidやiOSのコードを開くと右上に「Open for Editing in Android Studio」「open iOS module in Xcode」と表示され、このリンクを叩くことでそれぞれAndroid Studio/XCodeが別windowで立ち上がります。

スクリーンショット 2018-10-31 16.32.54.png

スクリーンショット 2018-10-31 16.32.44.png

これに従って、ネイティブコードはAndroid Studioやxcodeで開発をすることになります。


基本的な実装手順


Dart

先述のとおり、MethodChannelというものを使ってネイティブへメッセージを送信し、ネイティブ側で画面遷移を処理します。


main.dart

// MethodChannelには任意の名前をつけることができますが、慣習的にパッケージ名をprefixに使うようです

MethodChannel _methodChannel = MethodChannel('package.name/sample');

// ネイティブへのメッセージ送信>画面遷移
Future<Null> _launchNativeScreen() async {
// ネイティブ側へメッセージを送信
await _methodChannel.invokeMethod('test', "parameters");
}


ここで、MethodChannelのinvokeMethodは以下のような定義になっています。


platform_channel.dart

Future<dynamic> invokeMethod(String method, [dynamic arguments]) async


第一引数のmethodは、メッセージの種別を判別するための、任意の文字列です。

第二引数は、その際に渡したい引数の配列を指定します。上の例では"parameters"という文字列を渡しています。渡せるオブジェクトは基本的にプリミティブ型です。(詳しくは公式参照

そして、受け取るネイティブ側ではMethodChannelのnameと、invokeMethodのmethodを条件分岐に使って処理することになります。


Android

まずAndroidでの受け取り方です。

Flutterを表示しているMainActivityにてMethodChannelを使って受け取ります。


MainActivity

class MainActivity : FlutterActivity() {

companion object {
// main.dartでMethodChannelのコンストラクタで指定した文字列です
private const val CHANNEL = "package.name/sample"
// main.dartでinvokeMethodの第一引数に指定したmethodの文字列です
private const val METHOD_TEST = "test"
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)

// MethodChannelからのメッセージを受け取ります
// (flutterViewはFlutterActivityのプロパティ、CHANNELはcompanion objectで定義しています)
MethodChannel(this.flutterView, CHANNEL)
.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
if (methodCall.method == METHOD_TEST) {
// invokeMethodの第二引数で指定したパラメータを取得できます
val parameters = methodCall.arguments<String>()
launchAndroidScreen(parameters)
}
}
}
}

private fun launchAndroidScreen(parameters: String) {
startActivity(...)
}
}



iOS

続いてiOSでの受け取り方です。


AppDelegate.swift

@UIApplicationMain

@objc class AppDelegate: FlutterAppDelegate {

/// main.dartでMethodChannelのコンストラクタで指定した文字列です
private let methodChannelName = "package.name/sample"
/// main.dartでinvokeMethodの第一引数に指定したmethodの文字列です
private let methodTest = "test"

private var flutterViewController: FlutterViewController {
return self.window.rootViewController as! FlutterViewController
}

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self)

// MethodChannelはAndroidと同様、名前とFlutterViewControllerから生成します
let methdoChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: flutterViewController)
// MethodChannelからのメッセージを受け取ります
methdoChannel.setMethodCallHandler { [weak self] methodCall, result in
if methodChannel.method == methodTest {
// invokeMethodの第二引数で指定したパラメータを受け取れます
let parameters = methodCall.arguments as? String
self?.launchiOSScreen(parameters)
}
}

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

private func launchiOSScreen(_ parameters: String?) {
...
}
}


雰囲気はAndroidと同じですね。

単純にネイティブの画面を表示をしたい、というだけであればこれで十分かと思います。


画面遷移のあと、パラメータを受け取りたい場合

上記では単純な画面遷移しか紹介しませんでしたが、画面を戻ってきたときにデータを受け取ったりすることも可能です。

これは画面遷移ではなく、OSのAPIを使った処理の結果を返す場合なども同じです。


Dart


Main.dart

// ネイティブへのメッセージ送信>画面遷移

Future<Null> _launchNativeScreen() async {
// ネイティブ側へメッセージを送信
try {
final String result = await _methodChannel.invokeMethod('test', "parameters");
// 戻り値を使って処理を行う
...
} on PlatformException catch (e) {
// 必要に応じてエラー処理
}
}


Android

onActivityResultを使って戻り値を受け取り、それをMethodChannel.Resultに渡してあげる形になります。


MainActivity

class MainActivity : FlutterActivity() {

companion object {
...
private const val REQUEST_CODE = 1
private const val EXTRA_STRING = "extra_string"
}

// Dart側へデータを返却するためのオブジェクト
private var result: MethodChannel.Result?

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)

MethodChannel(this.flutterView, CHANNEL)
.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
this.result = result
if (methodCall.method == METHOD_TEST) {
val parameters = methodCall.arguments<String>()
launchAndroidScreen(parameters)
} else {
// Dart側に未実装であることを伝えます(ユーザから見ると、何も起こりません)
result.notImplemented()
}
}
}
}

private fun launchAndroidScreen(parameters: String) {
let intent = Intent(this, NextActivity.class)
intent.putExtra(MainActivity.EXTRA_STRING, parameters)
startActivityForResult(intent, REQUEST_CODE)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == COUNT_REQUEST) {
if (resultCode == RESULT_OK) {
// Dart側にsuccessとして結果を渡します
result.success("result")
} else {
// error通知(第三引数は、任意のオブジェクトを渡せます)
result.error("ErrorCode", "ErrorMessage", null)
}
}
}
}


MethodChannel.Resultの定義はこちらです。success、error時ともにオブジェクトを渡すことができます。また、notImplementedというメソッドも用意されています。


MethodChannel.java

public interface Result {

void success(@Nullable Object var1);

void error(String var1, @Nullable String var2, @Nullable Object var3);

void notImplemented();
}



iOS

FlutterResultというオブジェクトがFlutterMethodCallHandlerから返却されるので、それに対してメッセージを通知します。

Androidとはやや異なり、FlutterResultは任意の型を受け取れるようになっており、直接オブジェクトを渡してやることになります。下記のサンプルでは、エラー時は明示的にFlutterErrorというオブジェクトを生成して返却し、正常時はdelegate経由で受け取ったString型のデータをそのまま渡しています。


AppDelegate.swift

@UIApplicationMain

@objc class AppDelegate: FlutterAppDelegate {

/// main.dartでMethodChannelのコンストラクタで指定した文字列です
private let methodChannelName = "package.name/sample"
/// main.dartでinvokeMethodの第一引数に指定したmethodの文字列です
private let methodTest = "test"

// MethodChnnelの結果通知に使います
private var result: FlutterResult?

private var flutterViewController: FlutterViewController {
return self.window.rootViewController as! FlutterViewController
}

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self)

// MethodChannelはAndroidと同様、名前とFlutterViewControllerから生成します
let methdoChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: flutterViewController)
// MethodChannelからのメッセージを受け取ります
methdoChannel.setMethodCallHandler { [weak self] methodCall, result in
if methodChannel.method == methodTest {
// invokeMethodの第二引数で指定したパラメータを受け取れます
let parameters = methodCall.arguments as? String
self?.launchIOSScreen(parameters)
} else {
// 任意のオブジェクトを返却できます。ここでは明示的にFlutterErrorというオブジェクトを返却しています
result?(FlutterError(code: "ErrorCode", message: "ErrorMessage",details: nil))
}
}

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

private func launchIOSScreen(_ parameters: String?) {
let next: NextScreenViewController = ...
next.delegate = self
flutterViewController.present(next, animated: true, completion: nil)
}
}

extension AppDelegate: NextScreenViewControllerDelegate {
func nextScreenViewControllerSendMessage(_ viewController: NextScreenViewController, message: String) {
// 任意のオブジェクトを通知できます
result?(message)
}
}



NextScreenViewController.swift

class NextScreenViewController: UIViewController {

weak var delegate: NextScreenViewControllerDelegate? = nil
...
}

protocol NextScreenViewControllerDelegate {
func nextScreenViewControllerSendMessage(_ viewController: NextScreenViewController, message: String)
}



その他


Androidでcouldn't locate lint-gradle-api-26.1.2.jar for flutter projectとビルドエラーになってしまう

(1.0では修正されているようですが、念の為残しておきます。)

Stack Overflowにも同じ記事がありました。flutterのgradleファイルの記述を変更することで対応できました。

宣言順の問題のようです。

jcenterの前に'https://dl.google.com/dl/android/maven2' を定義する必要があるようです。

.flutter/packages/flutter_tools/gradle/flutter.gradleを以下のように修正してください。


flutter.gradle

    buildscript {

repositories {
+ jcenter()
maven {
url 'https://dl.google.com/dl/android/maven2'
}
- jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
}
}

https://stackoverflow.com/questions/52945041/couldnt-locate-lint-gradle-api-26-1-2-jar-for-flutter-project


1.1.10-pre.xxx ではiosのMethodChannelで落ちる

(2019/1/21追記)

現時点で最新の安定版は1.1.9ですが、1.1.10-pre.151でiosのMethodChannelを動かしたところ、画面をタッチするとクラッシュする事象に遭遇しました。

公式のサンプルでも同様にクラッシュするため、アップデートは慎重におねがいします。


EventChannel

MethodChannelとは別で、EventChannelというものも存在します。サンプルが公式でありますので、こちらを参考にどうぞ。

※時間があれば後日こちらの説明もしてみようかと思います。

https://github.com/flutter/flutter/tree/master/examples/platform_channel


参考

公式ドキュメント

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

obj-cやJavaですが、公式のサンプルです。

https://github.com/flutter/flutter/tree/master/examples/platform_view