9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Flutter大学Advent Calendar 2021

Day 17

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

Posted at

##開発環境

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を使います。今回は割愛します。)

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

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

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

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

##参考
今回はネイティブビューの表示がわかりやすいビデオ通話の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

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

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

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

##iOS

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

open ios/Runner.xcworkspace

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

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に登録します。
これで準備は完了です。

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)
  }
}

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

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を開いたが書きやすいです。

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

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) }
    }

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

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({関数名})**でネイティブ側の関数を呼び出します。
これでネイティブ側のコードを呼び出せるようになりました。
たったこれだけです。

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

#PlatformViewの書き方

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

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の文字列と同じものです。

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

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

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側なので今回は割愛します。)

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側は終わりです。

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ファイルに定数化しています。)

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

PlatformViewFactoryを作成します。
SkywayのSDKが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を表示します。

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です。

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で使いやすくなったっぽいです!

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

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

9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?