Flutter EventChannel APIの使い方

# はじめに

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

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

# 1. EventChannelとは

プラットフォーム側からFlutter (Dart) 側にイベント通知するためのAPIの一つである[EventChannel](https://api.flutter.dev/flutter/services/EventChannel/EventChannel.html)について解説します。

加速度センサーの値の変化やプラットフォーム側のイベントサイクルの変化など、プラットフォーム側で定期的もしくは突発的に発生するイベントをFlutter側に通知するためのAPIです。Flutterの仕組み的には、[ここ](https://qiita.com/kurun_pan/items/db6c8fa94bbfb5c0c8d7#2-methodchannel%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF)で解説しているPlatform Channelsを利用しています。

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

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

EventChannelはプラットフォーム側からDart側へのみの一方通行の1対多?の通信APIです。MethodChannelとは異なり、逆方向は出来ません。AndroidフレームワークのBroadcastのイメージを持っていただければ分かり易いかと思います。

### Dart側
1. [EventChannel](https://api.flutter.dev/flutter/services/EventChannel/EventChannel.html)#[receiveBroadcastStream](https://api.flutter.dev/flutter/services/EventChannel/receiveBroadcastStream.html)でプラットフォーム側のイベント受信用のリスナーを登録
2. 受信を終了時、[StreamSubscription]([StreamSubscription](https://api.flutter.dev/flutter/dart-async/StreamSubscription-class.html))#[cancel](https://api.dart.dev/stable/2.7.1/dart-async/StreamSubscription/cancel.html)をコール

### プラットフォーム側
1. [EventChannel](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.html)#[setStreamHandler](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.html#setStreamHandler-io.flutter.plugin.common.EventChannel.StreamHandler-)でイベントリスナーを登録
2. [EventChannel](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.html)の初期化が完了後、onListerがコールされるので、その引数の[EventSink](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html)をローカルに保存
3. [EventSink](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html)#[success](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html#success-java.lang.Object-)でイベント送信
4. 全てのイベント送信完了後に[endOfStream](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html#endOfStream--)をコール

<img width="1206" alt="スクリーンショット 2020-02-09 15.24.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/56b32f4a-ece8-0ba3-bb2c-a722921f3162.png">

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

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

## 4.1 Dart側の実装

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

### イベント受信リスナーの登録
作成したチャンネルの[receiveBroadcastStream](https://api.flutter.dev/flutter/services/EventChannel/receiveBroadcastStream.html)で[StreamSubscription](https://api.flutter.dev/flutter/dart-async/StreamSubscription-class.html)インスタンスが取得出来ます。このインスタンスの[listen](https://api.flutter.dev/flutter/dart-async/Stream/listen.html)メソッドでイベント受信リスナーを登録します。

[listen](https://api.flutter.dev/flutter/dart-async/Stream/listen.html)メソッドのcancelOnError引数は、プラットフォームからの送信イベントがエラー時に自動的に通信を終了 (cancel) するかどうかの設定です。

```Dart:listenメソッド
StreamSubscription<T> listen(void onData(T event),
    {Function onError, void onDone(), bool cancelOnError});
```

### 受信終了

これ以上受信が不要になった場合には、[StreamSubscription](https://api.flutter.dev/flutter/dart-async/StreamSubscription-class.html)#[cancel](https://api.dart.dev/stable/2.7.1/dart-async/StreamSubscription/cancel.html)をコールします。すると、プラットフォーム側にもキャンセルされたことが通知され、通信が完全に終了します。

### サンプルコード

```Dart:Dartサンプルコード
class _MyHomePageState extends State<MyHomePage> {
  static const EventChannel _channel = const EventChannel('com.example.eventchannel/interop');

  StreamSubscription _streamSubscription;
  String _platformMessage;

  void _enableEventReceiver() {
    _streamSubscription = _channel.receiveBroadcastStream().listen(
        (dynamic event) {
          print('Received event: $event');
          setState(() {
            _platformMessage = event;
          });
        },
        onError: (dynamic error) {
          print('Received error: ${error.message}');
        },
        cancelOnError: true);
  }

  void _disableEventReceiver() {
    if (_streamSubscription != null) {
      _streamSubscription.cancel();
      _streamSubscription = null;
    }
  }

  @override
  initState() {
    super.initState();
    _enableEventReceiver();
  }

  @override
  void dispose() {
    super.dispose();
    _disableEventReceiver();
  }
```

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

### チャンネル生成とイベントリスナー登録

[EventChannel](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.html)インスタンスを生成し、[EventChannel](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.html)#[setStreamHandler](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.html#setStreamHandler-io.flutter.plugin.common.EventChannel.StreamHandler-)でイベントリスナーを登録します。

### 初期化処理

初期化処理が完了し、イベント送信の準備が出来たタイミングでコールされます。このタイミングでイベント送信に関する初期化処理などを行うと良いでしょう。
引数の[EventSink](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html)はローカルに保存しておき、必要なタイミングでイベント送信します。

### イベント送信

```Kotlin:イベント送信例
eventSink?.success("Android")
```
のように引数を添えてイベント通知します。

### イベント送信終了

全てのイベントを送信完了し、これ以上イベント送信が不要な場合には[endOfStream](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html#endOfStream--)を利用します。これをコールすることで、プラットフォーム側からのトリガーで通信を終了することが出来ます。コール後、onCancelのイベントリスナーがコールされます。

### エラー発生時

想定外のエラーが発生するなどのこれ以上通信が不可になった場合など、[EventSink](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html)#[error](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html#error-java.lang.String-java.lang.String-java.lang.Object-)をコールすればDart側の[StreamSubscription](https://api.flutter.dev/flutter/dart-async/StreamSubscription-class.html)#[onError](https://api.flutter.dev/flutter/dart-async/StreamSubscription/onError.html)がコールされます。

```Kotlin:errorメソッド
void error(String errorCode, String errorMessage, Object errorDetails)
```

### サンプルコード

```Kotlin:Kotlinサンプルコード
class MainActivity: FlutterActivity() {
    private lateinit var channel: EventChannel
    var eventSink: EventSink? = null

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)

        channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.eventchannel/interop")
        channel.setStreamHandler(
                object : StreamHandler {
                    override fun onListen(arguments: Any?, events: EventSink) {
                        eventSink = events
                        Log.d("Android", "EventChannel onListen called")
                        Handler().postDelayed({
                            eventSink?.success("Android")
                            //eventSink?.endOfStream()
                            //eventSink?.error("error code", "error message","error details")
                        }, 500)
                    }
                    override fun onCancel(arguments: Any?) {
                        Log.w("Android", "EventChannel onCancel called")
                    }
                })
    }
}
```