Flutter MethodChannel APIの使い方

# はじめに

FlutterはクロスプラットフォームなUIフレームワークです。UI特化のため、UI作る以外のプラットフォーム固有の機能を利用する場合には、公開されているライブラリ (プラグイン) を利用する以外にはFlutterの専用APIを利用してプラットフォーム側の実装を行う必要があります。

プラットフォーム固有機能の例は以下です。

* Audio/Videoなどのメディアのデコード, エンコード, 再生
* ランタイムパーミッションの表示や設定確認
* Bluetoothや加速度センサーなどのハードウェア機能
* WebView
* ストレージやファイルアクセス

Dart⇆プラットフォーム双方向でまとまった情報が無かったため、ここにまとめることにしました。
今回はいくつかあるAPIのうち、最もよく使う[MethodChannel](https://api.flutter.dev/flutter/services/MethodChannel-class.html)について使い方を解説します。

## 関連記事

Flutter (Dart) とプラットフォーム (Android/iOSなど) 間の通信/呼び出しAPIについてまとめています。

* MethodChannel　←　今回
* [EventChannel](https://qiita.com/kurun_pan/items/6d63ebf1e894d3620b20)
* [MessageChannel](https://qiita.com/kurun_pan/items/9c2b6fdba602203f8f83)

# 1. Flutter MethodChannelとは

Dartからプラットフォーム (Android/iOS等) のメソッドを呼び出すもしくは、プラットフォームからDartのメソッドを呼び出すためのAPIです。イメージ的には、AndroidのJNI (Java/KotlinからC/C++を呼び出すI/F) に近いですが、違いはとても簡単に呼び出せるのと非同期APIだという点です。

# 2. MethodChannelの仕組み

Flutterのアーキテクチャを以下に示します。
<img width="585" alt="スクリーンショット 2020-02-16 18.56.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/b76a507b-835b-e3bf-826c-bb41ac1811bf.png">

MethodChannel APIの使い方は後述しますが、呼び出し先のメソッド名と引数のデータの2つを引数として渡します。Flutter Framework内ではMethodChannelをBinaryMessagesという形に変換し、Flutter Engineとメッセージパッシングのやり取りを行います。

この界面でAPIコールが非同期のメッセージパッシング (データ送受信) に変わります。

Flutter Engineはデータを受け取ると、そのデータをMethod ChannelのAPIの形に変更し、対象のプラットフォームのAPIをコールします。この仕組みを実現しているのがFlutter Engine内に存在するPlatform Channelsです。
<img width="320" alt="スクリーンショット 2020-02-10 21.29.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/bed29028-6ff6-e98a-f1c0-a0a00e80e0f6.png">

# 3. 基本フロー

実際のコードの説明の前に、APIの基本的なフローについて解説します。ただし、プラットフォーム毎に若干API名が異なる場合があるため (iOS) 、注意してください。

## 3.1 Dart → プラットフォーム

* [プラットフォーム側] MethodChannel#setMethodCallHandlerでコールバックを登録
* [Dart側] MethodChannel#invokeMethodで呼び出したいメソッド名とデータをセットして非同期でコール
* [プラットフォーム側] 受け取ったデータ (メソッド名) を見て、対象の処理を実施し、Result#success (エラーの場合はerror) をコール
* [Dart側] メソッドコールの結果を確認

<img width="1053" alt="スクリーンショット 2020-02-11 18.20.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/b2b6728b-a1e4-c24a-93c6-98499a10a745.png">

## 3.2 プラットフォーム → Dart

プラットフォーム側からDartを呼び出すことも出来ます。Dart → プラットフォームの場合と手順は同じです。

<img width="1053" alt="スクリーンショット 2020-02-11 18.20.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/fef931e6-f068-829b-0acf-f93ef802b628.png">

# 4. サンプルプロジェクト
実際のAPIの使い方は後述しますが、サンプルプロジェクトを以下に用意しています。そのまま動作確認可能ですので、参考にしてください。
ただし、正式に何かの機能を実装する場合は、プラグインの形で組み込んだ方が良いです。
https://github.com/Kurun-pan/flutter-methodchannel-example

# 5. Flutter + Android (Kotlin) のコード解説

## 5.1 Dartからプラットフォーム (Kotlin) を呼び出すケース

### 5.1.1 Dart側の実装

#### 通信チャンネル作成
MethodChannelで通信チャンネルを作成します。引数に指定する文字列は他のアプリケーションと被らず、一意に決まるようにするために慣例で"アプリパッケージ名/チャンネル名"とするのが一般的な様子です。

#### プラットフォーム側の呼び出し
チャンネル作成後はそのチャンネル#[invokeMethod](https://api.flutter.dev/flutter/services/MethodChannel/invokeMethod.html)を非同期でコールすることで、Kotlin側を呼び出すことが出来ます。
APIの仕様は引数の仕様は以下のようになっています。

 * 第一引数に呼び出したいメソッド名の文字列を指定
 * 第二引数には呼び出しメソッドの引数に指定するデータを指定。
  * 型はプリミティブ型で使える型に制約あり ([StandardMessageCodec](https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html))
  * 複数の引数を指定指定したい場合、JSON形式のようにMapで記述するのが便利かと

```dart:invokeMethod仕様
Future<T> invokeMethod<T>(String method, [ dynamic arguments ]) async
```

#### サンプルコード
実際のサンプルコードを以下に示します。

```dart:main.dart
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とプラットフォーム間で受け渡し可能な型には限りがあるため、詳細は[こちら](https://flutter.dev/docs/development/platform-integration/platform-channels#codec)を参照して下さい。もし非サポートの型を指定すると、実行時にエラーになります。

#### サンプルコード

```kotlin:Kotlinコード
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仕様を掲載しておきます。

```Kotlin:MethodChannel.Result.success仕様
@UiThread
void success(@Nullable Object result)
```

```Kotlin:MethodChannel.Result.error仕様
@UiThread
void error(String errorCode,
                     @Nullable
                     String errorMessage,
                     @Nullable
                     Object errorDetails)
```

```Kotlin:MethodChannel.Result.notImplemented仕様
@UiThread
void notImplemented()
```

## 5.2 プラットフォーム (Kotlin) からDartを呼び出す

基本的にDartからプラットフォームを呼び出す方法と同じです。このメソッドの実行は必ずUIスレッドから行ってください。

### プラットフォーム側の実装

作成したチャンネルに対して、invokeMethodをコールするだけです。
引数もプリミティブ型で渡すことが可能で、Dart側からの結果もMethodChannel.Resultのコールバックで受け取ることが可能です。不要なら指定なしでOKです。

```Kotlin:Kotlinコード
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の指定方法は、よく分かっていません。知っている方がいたら教えてください…。

#### サンプルコード
```Dart:Dartコード
  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);
  }
```

# 6. 参考文献

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