はじめに
この記事は Flutter #2 Advent Calendar 2018 18日目の記事です。
Flutterを導入しようと思ったときに、ネイティブで画面を作れる方法を知っていると、精神的にも余裕が生まれてFlutter導入へのハードルも低くなるのかなと勝手に解釈してこの記事を書こうと思いました。
なお、タイトルおよびサンプルではネイティブの画面を表示することを目的としていますが、実際は画面を表示せずにAndroid/iOSのコードだけを呼んでOSのAPIを叩くだけなどの実装も可能です。
概要
MethodChannel
という機能を使って、Dart,iOS,Androidそれぞれの実装を行います。
公式に載っている構成はこちらです。
このように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で立ち上がります。
これに従って、ネイティブコードはAndroid Studioやxcodeで開発をすることになります。
基本的な実装手順
Dart
先述のとおり、MethodChannel
というものを使ってネイティブへメッセージを送信し、ネイティブ側で画面遷移を処理します。
// MethodChannelには任意の名前をつけることができますが、慣習的にパッケージ名をprefixに使うようです
MethodChannel _methodChannel = MethodChannel('package.name/sample');
// ネイティブへのメッセージ送信>画面遷移
Future<void> _launchNativeScreen() async {
// ネイティブ側へメッセージを送信
await _methodChannel.invokeMethod<void>('test', 'parameters');
}
ここで、MethodChannelのinvokeMethodは以下のような定義になっています。
@optionalTypeArgs
Future<T?> invokeMethod<T>(String method, [ dynamic arguments ])
第一引数のmethodは、メッセージの種別を判別するための、任意の文字列です。
第二引数は、その際に渡したい引数の配列を指定します。上の例ではparameters
という文字列を渡しています。渡せるオブジェクトは基本的にプリミティブ型です。(詳しくは公式参照)
そして、受け取るネイティブ側ではMethodChannelのnameと、invokeMethodのmethodを条件分岐に使って処理することになります。
Android
まずAndroidでの受け取り方です。
Flutterを表示しているMainActivityにてMethodChannel
を使って受け取ります。
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 configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// MethodChannelからのメッセージを受け取ります
// (flutterViewはFlutterActivityのプロパティ、CHANNELはcompanion objectで定義しています)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, 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(...)
}
}
(2020/12修正)
- 以前はonCreateで実装していましたが、現在は
configureFlutterEngine
で実装する方法が正しいです。
iOS
続いてiOSでの受け取り方です。
@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 methodChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: flutterViewController.binaryMessenger)
// MethodChannelからのメッセージを受け取ります
methodChannel.setMethodCallHandler { [weak self] methodCall, result in
if methodCall.method == self?.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
// ネイティブへのメッセージ送信>画面遷移
Future<Hoge> _launchNativeScreen() async {
// ネイティブ側へメッセージを送信
try {
final json = await _methodChannel.invokeMethod<Map<Object?, Object?>>('test', 'parameters');
// 戻り値を使って処理を行う
// ここではJson文字列をパースすることを想定した処理を書きます
if (json != null) {
final encode = jsonEncode(json);
final map = jsonDecode(encode) as Map<String, dynamic>;
return Hoge.fromJson(map);
}
...
} on PlatformException catch (e) {
// 必要に応じてエラー処理
}
}
MethodChannelの戻り値が、NSDictionaryやMapだったとしてもMap<String, dynamic>
にすることはできず、<Map<Object?, Object?>>
にする必要があります。そのため、パースするためにはjsonEncode/jsonDecode
を経由してMap<String, dynamic>
に変換してから利用します。
Android
onActivityResult
を使って戻り値を受け取り、それをMethodChannel.Result
に渡してあげる形になります。
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 configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, 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
というメソッドも用意されています。
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型のデータをそのまま渡しています。
@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)
}
}
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
を以下のように修正してください。
buildscript {
repositories {
+ jcenter()
maven {
url 'https://dl.google.com/dl/android/maven2'
}
- jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
}
}
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