はじめに
FlutterのPlatformView
機能を利用したView系のプラグインの作り方について解説します。
解説用の題材として、TextView
とWebView
を用いました。
Platform Viewsとは?
通常、FlutterはFlutterのフレームワークで用意されているウィジェットしか利用することが出来ません (ウィジェット一覧はWidget catalog) が、PlatformView機能を利用するとプラットフォーム (Android/iOS) 固有のViewをFlutterのウィジェットとして利用することが出来ます。
Flutterフレームワークは、このPlatformView
機能をAndroid, iOS向けに使い易いAPIにして機能を提供してくれています。それがそれぞれAndroidView
とUiKitView
です。基本的に開発者はこの機能を利用します。ソースコードはここです。
ただし、表示を行うだけであれば上記を利用すればOKですが、Platform側とやりとりを行うには独自で通信部分を作成する必要があり、その時にメインで利用するのがMethodChannel
です。MethodChannel
についてはFlutter MethodChannel APIの使い方を併せて参照してください。
Plaftorm Viewsの仕組み
OpenGLやVulkan等のグラフィックスの知識がある方は理解し易いかもしれませんが、プラットフォーム側で仮想画面 (サーフェス) に描画し、その描画データをFlutter側で取得し、Flutter側のレンダリングによりFlutter側の画面と合成し、最終的な画面を生成しています。
以下、Android版のPlatformViewのイメージ図です。
iOS側は実装を追いかけていませんが、Flutterの最後の秘宝、Platform Viewの実装を調べに行くが参考になると思います。おそらくiOSでもAndroidと同じようなことをやっていると思います。
基本的なプラグインの作成の流れ (View系の話に限定)
- 【プラットフォーム側】 PlatformViewを継承したプラグイン用のViewを作成
- 【プラットフォーム側】 PlatformViewFactoryを継承したクラスで上記のViewを生成
- 【プラットフォーム側】 FlutterPluginを継承したプラグイン登録クラス内でChannel登録
- 【プラットフォーム側】 MainActivityで上記プラグインクラスを登録
- 【Flutter側】 プラグイン対象Viewの独自ウィジェットを作成し、その中でAndroidViewウィジェットを生成
- 【Flutter側】 必要があれば、上記の独自ウィジェットの中でMethodChannelを利用して相互呼び出し
- 【Flutter側】 上記の独自ウィジェットを画面のレイアウトに組み込む
Flutter Framework内の実装的な部分では、System Channelsのplatform_viewを利用しています。リンク先にQiitaの記事を書いていますので、参考にしてください。
プラットフォーム (Android) 側のコード
プラットフォーム側について解説します。
1. Flutter用のWebViewプラグインView作成
io.flutter.plugin.platform.PlatformView
を継承したFlutterWebViewクラスを作成します。
内部ではWebView
をインスタンスし、MethodChannelでイベントが発生した時にその引数のデータ (URL) を開くというシンプルなサンプルです。
WebView
はAndroidX
を利用していますが、詳細はこちらを参照してください。同じくMethodChannelについてもこちらにまとめていますので、参照して下さい。
MethodChannelの注意点としては、各PlatformView事にFlutter側からint側のidが振られるため、それを意識したチャンネル名を指定する必要があることです。
package com.example.flutter_platform_view_app
import android.content.Context
import android.view.View
import android.webkit.WebView
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.platform.PlatformView
class FlutterWebView internal constructor(context: Context?, messenger: BinaryMessenger?, id: Int)
: PlatformView, MethodCallHandler {
private val webView: WebView
private val methodChannel: MethodChannel
init {
webView = WebView(context)
webView.apply {
settings.apply {
// enable Javascript
javaScriptEnabled = true
setSupportZoom(true)
builtInZoomControls = true
displayZoomControls = false // no zoom button
loadWithOverviewMode = true
useWideViewPort = true
domStorageEnabled = true
}
}
methodChannel = MethodChannel(messenger, "plugins.kurun.views/webview_$id")
methodChannel.setMethodCallHandler(this)
}
@Override
override fun getView(): View {
return webView
}
@Override
override fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) {
when (methodCall.method) {
"setUrl" -> setUrl(methodCall, result)
else -> result.notImplemented()
}
}
private fun setUrl(methodCall: MethodCall, result: MethodChannel.Result) {
val url = methodCall.arguments as String
webView.loadUrl(url)
result.success(null)
}
@Override
override fun dispose() {
webView.destroy()
}
}
2. PlatformViewFactoryクラスの作成
PlatformViewFactory
クラスを継承したWebVewFactoryクラスを用意します。
PlatformViewを利用する場合、必ずこのPlatformViewFactoryを用意する必要があります。この中で先ほど作成したFluterWebViewをインスタンスします。
package com.example.flutter_platform_view_app
import android.content.Context
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
class WebViewFactory(private val messenger: BinaryMessenger) :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, id: Int, o: Any?): PlatformView {
return FlutterWebView(context, messenger, id)
}
}
3. FlutterPluginクラスの作成
io.flutter.embedding.engine.plugins.FlutterPlugin
を継承したWebViewPluginクラスを用意します。このクラスを最終的にAndroidのTopからプラグインとして登録することになります。
package com.example.flutter_platform_view_app
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
class WebViewPlugin : FlutterPlugin {
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterPluginBinding.platformViewRegistry
.registerViewFactory(
"plugins.kurun.views/webview",
WebViewFactory(flutterPluginBinding.binaryMessenger)
)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
}
}
4. 作成したプラグインの登録
最後にプラグインを登録して完了です。
今回はMainActivityからWebViewPluginを登録します。
package com.example.flutter_platform_view_app
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
flutterEngine.plugins.add(WebViewPlugin())
}
}
Flutter (Dart) 側のコード
次にFlutterのDart側の対応について解説します。
5. WebViewウィジェットの作成
WebViewクラスを作成します。ポイントはAndroidViewを利用して先ほど用意したプラットフォーム側のWebViewプラグイン名を指定する点です。
AndroidViewのソースコードはこちら
サンプルコードを見ていただければ分かると思いますが、非常に簡単に利用が出来ます。
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
typedef void WebViewCreatedCallback(WebViewController controller);
class WebView extends StatefulWidget {
const WebView({
Key key,
this.onWebViewViewCreated,
}) : super(key: key);
final WebViewCreatedCallback onWebViewViewCreated;
@override
State<StatefulWidget> createState() => _WebViewState();
}
class _WebViewState extends State<WebView> {
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.kurun.views/webview',
onPlatformViewCreated: _onPlatformViewCreated,
);
}
return Text(
'$defaultTargetPlatform is not supported!');
}
void _onPlatformViewCreated(int id) {
if (widget.onWebViewViewCreated == null)
return;
widget.onWebViewViewCreated(new WebViewController._(id));
}
}
6. MethodChannelでのプラットフォーム側の呼び出し (データ送信)
サンプルコードは、onPlatformViewCreated
のコールバックのタイミングでプラットフォーム側のWebViewプラグインのMethodChannelを作成し、main.dart側から任意のURLを送信し出来るようにしました。
class WebViewController {
WebViewController._(int id)
: _channel = new MethodChannel('plugins.kurun.views/webview_$id');
final MethodChannel _channel;
Future<void> setUrl(String url) async {
assert(url != null);
return _channel.invokeMethod('setUrl', url);
}
}
7. 画面の作成
先ほど作成したWebViewウィジェットを好きに配置すれば完成です!
簡単ですよね?
※TextViewプラグインについては解説していませんが、以下のサンプルコードには出てきます…。基本的な作りはWebViewプラグインと同じです!
import 'package:flutter/material.dart';
import 'package:flutter_platform_view_app/text_view.dart';
import 'package:flutter_platform_view_app/web_view.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter PlatformView API',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter PlatformView Example'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Column(children: [
Center(
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.0),
width: 200.0,
height: 40.0,
child: TextView(
onTextViewCreated: _onTextViewCreated,
)
)
),
Expanded(
flex: 3,
child: WebView(
onWebViewViewCreated: _onWebViewCreated,
)
)
])
);
}
void _onTextViewCreated(TextViewController controller) {
controller.setText('Android TextView and WebView example.');
}
void _onWebViewCreated(WebViewController controller) {
controller.setUrl('https://www.google.co.jp/');
}
}
サンプルコード一式
上記で解説したソースコードのプロジェクトファイル一式を以下にUPしています。
Android環境であればそのまま動作するため、参考にしてください。
https://github.com/Kurun-pan/flutter-platformview-example