MethodChannelとPlatformViewを使ってFlutterにネイティブビューを表示しよう

##開発環境
```
Flutter 2.5.2
Dart 2.14.3
```

#MethodChannelとは？
Flutterは柔軟なシステムを使用しており、AndroidではKotlinまたはJava、iOSではSwiftまたはObjective-Cで利用できるかどうかにかかわらず、プラットフォーム固有のAPIを呼び出すことができます。

#PlatformViewとは？
PlatformViewを使用すると、ネイティブビューをFlutterアプリに埋め込むことができます。
たとえば、AndroidおよびiOSSDKのネイティブGoogleマップをFlutterアプリ内で直接使用できます。

（ネイティブ側からFlutter側へ受け取るにはEventChannelを使います。今回は割愛します。）


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527341/fd92db08-37db-b9f3-e6de-51ad51bee977.png" width="600">



##つまり…
MethodChannelでネイティブのAPIを呼び出し、PlatformViewでネイティブの画面をFlutterに表示することができる！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527341/45bb3993-b9d8-8cde-fc06-d0d831149ce9.png" width="600">

##ただし…
ネイティブに一度仮想的に描画してからFlutter側にコピーして再描画するため、パフォーマンスや描画の即応性は低いです。

ドキュメントにも、Flutterのみで完結できる場合はなるべくPlatformViewを使わない方が良い旨が書いてあります。

##いつ使うの？
あんまり使いません笑
FlutterにはMethodChennelやPlatformViewを使わないで済むような素晴らしいPluginが揃っています。（FaceIDやARKitですらあります！）
使いこなせれば、AndroidやiOSでしか提供されていないSDKを組み込んだり、端末自体の機能を操作できるようになるという感じです。
ドキュメントにはPluginを作成するやり方で書かれていますが、普通のFlutterプロジェクトでもできるので、今回はそれで作成します。

https://github.com/raisukeshirabelqbz/skyway_sample

##参考
今回はネイティブビューの表示がわかりやすいビデオ通話のSDKであるSkyWayをFlutterプロジェクトに組み込みます。
（ちなみにSkyWayはPluginのプロジェクトで行うとAndroid側のSDKである.aarが読み込めないのでFlutterプロジェクトでやります。）

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

###PlatformView
https://flutter.dev/docs/development/platform-integration/platform-views

###SkyWay
https://webrtc.ecl.ntt.com/

###実装は以下を参考にさせていただきました。
https://qiita.com/imamurh/items/43966e33e100956e0998

https://serenegiant.com/blog/?p=3879

#MethodChannelの書き方
まず、Flutter側にMethodChannelを準備します。
Channel名はネイティブ側と合わせれば任意の文字列で大丈夫です。


``` dart:lib/skyway_service.dart
const MethodChannel _channel = MethodChannel("skyway_service");
```

ネイティブ側でMethodChannelを受ける準備を行いましょう。

##iOS

まず、iOS側です。
SwiftなのでXcodeで書いたがわかりやすいです。
Xcodeを開きましょう。

``` terminal
open ios/Runner.xcworkspace
```

使用するクラスとファイルを作成します。（今回はRunner/SkywayService.swiftを作成）
register関数を作成し、先程Flutter側で準備した同じ文字列のMethodChannelを登録します。

```swift:ios/Runner/SkywayService.swift
class SkywayService: NSObject, FlutterPlugin {
    
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "skyway_service", binaryMessenger: registrar.messenger())
        let instance = SkywayService(messenger: registrar.messenger())
        registrar.addMethodCallDelegate(instance, channel: channel)
    }
```

作成したクラスをRunner/AppDelegateに登録します。
これで準備は完了です。

```swift:ios/Runner/AppDelegate.swift
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    SkywayService.register(with: registrar(forPlugin: "SkywayService")!)
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
```

あとは作成したクラスの中で使用するメソッドを書くだけです。（具体的な処理はコードの下の方に書いています。）

```swift:ios/Runner/SkywayService.swift
public func handle(_ methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
    enum Method: String {
        case connect
        case destroy
        case call
        case listAllPeers
        case enableAudio
        case enableVideo
        case switchCamera
    }

    guard let method = Method.init(rawValue: methodCall.method) else {
        result(FlutterMethodNotImplemented)
        return
    }
    switch method {
        case .connect:      self.connect(methodCall, result: result)
        case .destroy:      self.destroy(methodCall, result: result)
        case .call:         self.call(methodCall, result: result)
        case .listAllPeers: self.listAllPeers(methodCall, result: result)
        case .enableAudio:  self.enableAudio(methodCall, result: result)
        case .enableVideo:  self.enableVideo(methodCall, result: result)
        case .switchCamera: self.switchCamera(methodCall, result: result)
        }
    }
```

##Android
Android側です。kotlinなのでandroidリポジトリ配下からAndroidStudioを開いたが書きやすいです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527341/4487e452-c9fb-2176-767d-504f62454a09.png" width="600">

AndroidはMainActivityに直接Flutter側のMethodChannelを登録します。（これはConstファイルに定数化しています。）

```kotlin:android/app/src/main/MainActivity.kt
class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, Const.METHOD_CHANNEL_NAME)
                .setMethodCallHandler { call, result -> onMethodCall(call, result) }
    }
```

同じように使用するメソッドを書くだけです。（コードの下の方に具体的な処理は書いてます。）

```kotlin:android/app/src/main/MainActivity.kt
private fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) {
    when (methodCall.method) {
        "connect" -> {
            connect(methodCall, result)
        }
        "destroy" -> {
            destroy(methodCall, result)
        }
        "call" -> {
            call(methodCall, result)
        }
        "listAllPeers" -> {
            listAllPeers(methodCall, result)
        }
        "enableAudio" -> {
            enableAudio(methodCall, result)
        }
        "enableVideo" -> {
            enableVideo(methodCall, result)
        }
        "switchCamera" -> {
            switchCamera(methodCall, result)
        }
        else -> result.notImplemented()
    }
}
```

##Flutter
Flutter側は**invokeMethod({関数名})**でネイティブ側の関数を呼び出します。
これでネイティブ側のコードを呼び出せるようになりました。
たったこれだけです。

```dart:lib/skyway_service.dart
await _channel.invokeMethod('connect', {
  'apiKey': apiKey,
  'domain': domain,
  'peerId': peerId,
});
```

#PlatformViewの書き方

次にネイティブビューを表示しましょう。
Flutter側です。iOS側はUiKitViewを作成します。
Android側はAndroidViewを作成します。

```dart:lib/skyway_service.dart
Widget platformView({@required int? viewId}) {
    if (defaultTargetPlatform == TargetPlatform.iOS) {
      return UiKitView(
        viewType: 'skyway_service/video_view',
        onPlatformViewCreated: (id) {
          print('UiKitView created: id = $id');
        },
        creationParams: {
          'id': viewId,
        },
        creationParamsCodec: const StandardMessageCodec(),
      );
    } else if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(
        viewType: 'skyway_service/video_view',
        onPlatformViewCreated: (id) {
          print('AndroidView created: id = $id');
        },
        creationParams: {
          'id': viewId,
        },
        creationParamsCodec: const StandardMessageCodec(),
      );
    } else {
      return const Center(
        child: Text(
          'No supported by the plugin',
          style: TextStyle(
            color: Colors.white,
          ),
        ),
      );
    }
  }
```

##iOS
iOS側です。
PlatformViewもregister関数内に登録します。
Flutter側で作成したviewTypeの文字列と同じものです。

```swift:ios/Runner/SkywayService.swift
    registrar.register(instance, withId: "skyway_service/video_view")
```

PlatformViewにはPlatformViewFactoryとPlatformViewが必要です。PlatformViewFactoryを作成します。

```swift:ios/Runner/SkywayService.swift
extension SkywayService: FlutterPlatformViewFactory {
    // UIKitViewのcreationParamsの値をcreate関数のargsに渡す
    public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
        FlutterStandardMessageCodec.sharedInstance()
    }
    // switchViewでidによって自分の画像か相手の画像か判別する
    public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
        guard let map = args as? Dictionary<String, Any?> else {
            return SkywayServicePlatformView(errorView)
        }
        let view = switchView(frame: frame, args: map as Dictionary<String, Any>)
        return SkywayServicePlatformView(view)
    }
```

PlatformViewFactoryの中で、Skywayから受け取ったMediaConnectionをネイティブに表示しています。（MediaConnectionをネイティブに表示するのは、Skyway側なので今回は割愛します。）

```swift:ios/Runner/SkywayService.swift
private func switchView(frame: CGRect, args: Dictionary<String, Any>) -> UIView {
        let id = args["id"] as! Int
        switch id {
        case 0:
            localView.frame = frame
            localView.backgroundColor = .black
            if let view = peers.first?.value.localStreamView {
                view.frame = localView.bounds
                view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
                localView.addSubview(view)
            }
            return localView
        case 1:
            remoteView.frame = frame
            remoteView.backgroundColor = .black
            if let view = peers.first?.value.remoteStreamView {
                view.frame = remoteView.bounds
                view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
                remoteView.addSubview(view)
            }
            return remoteView
        default:
            errorView.backgroundColor = .red
            return errorView
        }
    }
```

PlatformViewです。
以上でiOS側は終わりです。

```swift:ios/Runner/SkywayService.swift
class SkywayServicePlatformView: NSObject, FlutterPlatformView {
    let platformView: UIView
    init(_ platformView: UIView) {
        self.platformView = platformView
        super.init()
    }
    func view() -> UIView {
        return platformView
    }
}
```


##Android
Android側です。やり方はiOSと一緒です。
MainActivity内にFlutter側で作成したviewTypeを登録します。
（Constファイルに定数化しています。）

```kotlin:android/app/src/main/MainActivity.kt
flutterEngine.platformViewsController.registry.registerViewFactory(Const.SKYWAY_SERVICE_VIEW,SkywayServiceFactory(flutterEngine.dartExecutor.binaryMessenger))
```

PlatformViewFactoryを作成します。
SkywayのSDKがJavaなのでJavaで書いています。

```java:android/app/src/main/SkywayServiceFactory.java
public class SkywayServiceFactory extends PlatformViewFactory {
    @NonNull
    private final BinaryMessenger messenger;

    public SkywayServiceFactory(@NotNull BinaryMessenger messenger) {
        super(StandardMessageCodec.INSTANCE);
        this.messenger = messenger;
    }

    @Override
    public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
        if (args instanceof HashMap) {
            @SuppressWarnings("unchecked")
            final HashMap<String, Object> creationParams = (HashMap<String, Object>) args;
            final Canvas view;
            view = switchView(context, creationParams);
            return new SkywayServiceView(view);
        }
        throw new IllegalStateException("args is null");
    }
```

こちらもSkywayから受け取ったMediaConnectionを表示します。

```java:android/app/src/main/SkywayServiceFactory.java
private Canvas switchView(Context context, Map<String, Object> creationParams) {
        final int id = (int) creationParams.get("id");
        if (id == 0) {
            return SkywayPeer.localStreamView;
        } else if (id == 1) {
            return SkywayPeer.remoteStreamView;
        } else {
            final Canvas errorView = new Canvas(context);
            errorView.setBackgroundColor(Color.RED);
            return errorView;
        }
    }
}
```

最後にPlatformViewです。

```java:android/app/src/main/SkywayServiceView.java
public class SkywayServiceView implements PlatformView {

    private final Canvas view;

    SkywayServiceView(Canvas canvas) {
        view = canvas;
    }

    @Override
    public View getView() {
        return view;
    }

    @Override
    public void dispose() {
    }
}
```

#最後に

かなりざっくりした説明です。
よし！PlatformViewやってみようという方！
Flutter2.8で使いやすくなったっぽいです！

https://docs.flutter.dev/development/platform-integration/platform-views

あと、ビデオ通話をやってみたいという方！
SkyWayではなくagoraがおすすめです。
FlutterのSDKがあります！

https://jp.vcube.com/service/agora/developer/tutorial/messaging/flutter.html

ほんとだったら両方書かなくちゃいけないのに
Flutterだけで両プラットフォームが作れるという素晴らしさを理解できました！
2つ管理するのは超大変です。
みなさんFlutterを使いましょう！笑