はじめに
FlutterはクロスプラットフォームなUIフレームワークです。UI特化のため、UI作る以外のプラットフォーム固有の機能を利用する場合には、公開されているライブラリ (プラグイン) を利用する以外にはFlutterの専用APIを利用してプラットフォーム側の実装を行う必要があります。
プラットフォーム固有機能の例は以下です。
- Audio/Videoなどのメディアのデコード, エンコード, 再生
- ランタイムパーミッションの表示や設定確認
- Bluetoothや加速度センサーなどのハードウェア機能
- WebView
- ストレージやファイルアクセス
Dart⇆プラットフォーム双方向でまとまった情報が無かったため、ここにまとめることにしました。
今回はいくつかあるAPIのうち、最もよく使うMethodChannelについて使い方を解説します。
関連記事
Flutter (Dart) とプラットフォーム (Android/iOSなど) 間の通信/呼び出しAPIについてまとめています。
- MethodChannel ← 今回
- EventChannel
- MessageChannel
1. Flutter MethodChannelとは
Dartからプラットフォーム (Android/iOS等) のメソッドを呼び出すもしくは、プラットフォームからDartのメソッドを呼び出すためのAPIです。イメージ的には、AndroidのJNI (Java/KotlinからC/C++を呼び出すI/F) に近いですが、違いはとても簡単に呼び出せるのと非同期APIだという点です。
2. MethodChannelの仕組み
MethodChannel APIの使い方は後述しますが、呼び出し先のメソッド名と引数のデータの2つを引数として渡します。Flutter Framework内ではMethodChannelをBinaryMessagesという形に変換し、Flutter Engineとメッセージパッシングのやり取りを行います。
この界面でAPIコールが非同期のメッセージパッシング (データ送受信) に変わります。
Flutter Engineはデータを受け取ると、そのデータをMethod ChannelのAPIの形に変更し、対象のプラットフォームのAPIをコールします。この仕組みを実現しているのがFlutter Engine内に存在するPlatform Channelsです。
3. 基本フロー
実際のコードの説明の前に、APIの基本的なフローについて解説します。ただし、プラットフォーム毎に若干API名が異なる場合があるため (iOS) 、注意してください。
3.1 Dart → プラットフォーム
- [プラットフォーム側] MethodChannel#setMethodCallHandlerでコールバックを登録
- [Dart側] MethodChannel#invokeMethodで呼び出したいメソッド名とデータをセットして非同期でコール
- [プラットフォーム側] 受け取ったデータ (メソッド名) を見て、対象の処理を実施し、Result#success (エラーの場合はerror) をコール
- [Dart側] メソッドコールの結果を確認
3.2 プラットフォーム → Dart
プラットフォーム側からDartを呼び出すことも出来ます。Dart → プラットフォームの場合と手順は同じです。
4. サンプルプロジェクト
実際のAPIの使い方は後述しますが、サンプルプロジェクトを以下に用意しています。そのまま動作確認可能ですので、参考にしてください。
ただし、正式に何かの機能を実装する場合は、プラグインの形で組み込んだ方が良いです。
https://github.com/Kurun-pan/flutter-methodchannel-example
5. Flutter + Android (Kotlin) のコード解説
5.1 Dartからプラットフォーム (Kotlin) を呼び出すケース
5.1.1 Dart側の実装
通信チャンネル作成
MethodChannelで通信チャンネルを作成します。引数に指定する文字列は他のアプリケーションと被らず、一意に決まるようにするために慣例で"アプリパッケージ名/チャンネル名"とするのが一般的な様子です。
プラットフォーム側の呼び出し
チャンネル作成後はそのチャンネル#invokeMethodを非同期でコールすることで、Kotlin側を呼び出すことが出来ます。
APIの仕様は引数の仕様は以下のようになっています。
- 第一引数に呼び出したいメソッド名の文字列を指定
- 第二引数には呼び出しメソッドの引数に指定するデータを指定。
- 型はプリミティブ型で使える型に制約あり (StandardMessageCodec)
- 複数の引数を指定指定したい場合、JSON形式のようにMapで記述するのが便利かと
Future<T> invokeMethod<T>(String method, [ dynamic arguments ]) async
サンプルコード
実際のサンプルコードを以下に示します。
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
static const MethodChannel _channel = const MethodChannel('com.example.methodchannel/interop');
static Future<dynamic> get _list async {
final Map params = <String, dynamic> {
'name': 'my name is hoge',
'age': 25,
};
final List<dynamic> list = await _channel.invokeMethod('getList', params);
return list;
}
@override
initState() {
super.initState();
// Dart -> Platforms
_list.then((value) => print(value));
}
5.1.2 プラットフォーム側の実装
通信チャンネル作成
Dart側と同じように、MethodChannelで通信チャンネルを作成します。引数の文字列はDart側と同じにする必要があります。
メソッド名の取得
チャンネル作成後、MethodCallHanderのコールバックを設定します。コールバックメソッドの引数methodCall.metodに、Dart側のinvokeMethodの第1引数の文字列が格納されているため、それを見て適切な処理を行います。
引数の取得
Dart側のinvokeMethodの第2引数は、methodCall.argumentから取得出来ます。ただし、Dartとプラットフォーム間で受け渡し可能な型には限りがあるため、詳細はこちらを参照して下さい。もし非サポートの型を指定すると、実行時にエラーになります。
サンプルコード
class MainActivity: FlutterActivity() {
companion object {
private const val CHANNEL = "com.example.methodchannel/interop"
private const val METHOD_GET_LIST = "getList"
}
private lateinit var channel: MethodChannel
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
if (methodCall.method == METHOD_GET_LIST) {
val name = methodCall.argument<String>("name").toString()
val age = methodCall.argument<Int>("age")
Log.d("Android", "name = ${name}, age = $age")
val list = listOf("data0", "data1", "data2")
result.success(list)
}
else
result.notImplemented()
}
}
Dart側に結果を返す
プラットフォーム側からDart側に結果を返す場合は、MethodCallHanderのコールバックメソッドの第2引数のMethodChannel.Resultクラスインスタンスをコールすることで実現できます。サンプルコードのようにリターン値を渡すことも可能です。
受け取ったResultインスタンスをローカルに保存し、処理を行った後で結果を返すことも可能ですが、その場合には必ずUIスレッドで結果を返す必要があります。
参考のために以下にResultクラスの各メソッドのAPI仕様を掲載しておきます。
@UiThread
void success(@Nullable Object result)
@UiThread
void error(String errorCode,
@Nullable
String errorMessage,
@Nullable
Object errorDetails)
@UiThread
void notImplemented()
5.2 プラットフォーム (Kotlin) からDartを呼び出す
基本的にDartからプラットフォームを呼び出す方法と同じです。このメソッドの実行は必ずUIスレッドから行ってください。
プラットフォーム側の実装
作成したチャンネルに対して、invokeMethodをコールするだけです。
引数もプリミティブ型で渡すことが可能で、Dart側からの結果もMethodChannel.Resultのコールバックで受け取ることが可能です。不要なら指定なしでOKです。
channel.invokeMethod("callMe", listOf("a", "b"), object : MethodChannel.Result {
override fun success(result: Any?) {
Log.d("Android", "result = $result")
}
override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) {
Log.d("Android", "$errorCode, $errorMessage, $errorDetails")
}
override fun notImplemented() {
Log.d("Android", "notImplemented")
}
})
result.success(null)
Dart側の実装
作成したチャンネル#setMethodCallHandlerでMethodCallを設定します。
メソッド名の取得
コールバック引数のcall.methodでプラットフォーム側から指定された第1引数の値を取得出来ます。
引数の取得
call.argumentsに格納されています。
結果のリターン
成功 (通常) の場合は、Future.valueで結果を返します。
Future.errorを利用すると、プラットフォーム側のMethodChannel.Result.errorがコールされます。
MethodChannel.Result.notImplementedの指定方法は、よく分かっていません。知っている方がいたら教えてください…。
サンプルコード
Future<dynamic> _platformCallHandler(MethodCall call) async {
switch (call.method) {
case 'callMe':
print('call callMe : arguments = ${call.arguments}');
return Future.value('called from platform!');
//return Future.error('error message!!');
default:
print('Unknowm method ${call.method}');
throw MissingPluginException();
break;
}
}
@override
initState() {
super.initState();
// Platforms -> Dart
_channel.setMethodCallHandler(_platformCallHandler);
}