##開発環境
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名はネイティブ側と合わせれば任意の文字列で大丈夫です。
const MethodChannel _channel = MethodChannel("skyway_service");
ネイティブ側でMethodChannelを受ける準備を行いましょう。
##iOS
まず、iOS側です。
SwiftなのでXcodeで書いたがわかりやすいです。
Xcodeを開きましょう。
open ios/Runner.xcworkspace
使用するクラスとファイルを作成します。(今回はRunner/SkywayService.swiftを作成)
register関数を作成し、先程Flutter側で準備した同じ文字列のMethodChannelを登録します。
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に登録します。
これで準備は完了です。
@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)
}
}
あとは作成したクラスの中で使用するメソッドを書くだけです。(具体的な処理はコードの下の方に書いています。)
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ファイルに定数化しています。)
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) }
}
同じように使用するメソッドを書くだけです。(コードの下の方に具体的な処理は書いてます。)
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({関数名})**でネイティブ側の関数を呼び出します。
これでネイティブ側のコードを呼び出せるようになりました。
たったこれだけです。
await _channel.invokeMethod('connect', {
'apiKey': apiKey,
'domain': domain,
'peerId': peerId,
});
#PlatformViewの書き方
次にネイティブビューを表示しましょう。
Flutter側です。iOS側はUiKitViewを作成します。
Android側はAndroidViewを作成します。
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の文字列と同じものです。
registrar.register(instance, withId: "skyway_service/video_view")
PlatformViewにはPlatformViewFactoryとPlatformViewが必要です。PlatformViewFactoryを作成します。
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側なので今回は割愛します。)
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側は終わりです。
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ファイルに定数化しています。)
flutterEngine.platformViewsController.registry.registerViewFactory(Const.SKYWAY_SERVICE_VIEW,SkywayServiceFactory(flutterEngine.dartExecutor.binaryMessenger))
PlatformViewFactoryを作成します。
SkywayのSDKがJavaなので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を表示します。
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です。
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を使いましょう!笑