版本
Flutter版本2.0.3
GitHub源代码
本文目标
成功在Flutter页面中嵌入Android原生控件
基本步骤
- 准备Native UI的四件套
- 注册Native UI
- 准备flutter/platform_views
- Dart侧对应widget封装
准备Native UI的四件套
要想将Native UI(view) 嵌入到Flutter中,首先我们需要准备Native UI的四件套,来一起看下什么是Native UI的四件套
- PlatformView:要嵌入到Flutter的iOS view 或 Android view
- PlatformViewController:PlatformView的控制器,用来创建和管理PlatformView
- PlatformViewFactory:用于向Flutter提供PlatformView
- PlatformViewPlugin:用于向Flutter注册PlatformView
对于Flutter而言,无论是Android 还是iOS将Native UI(View)嵌入到Flutter中使用的原理和步骤是相同的,接下来我们以Android为例子来将ImageView控件嵌入到Flutter页面中进行使用
准备PlatformView
class FImageView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) :
AppCompatImageView(context, attrs, defStyleAttr) {
fun setUrl(url: String) {
Glide.with(this).load(url).into(this)
}
}
这里,我们使用Glide进行图片的加载,大家可以根据需要选择适合自己的图片加载库
准备PlatformViewController
class FImageViewController(context: Context, messenger: BinaryMessenger, id: Int?, args: Any?) :
PlatformView, MethodChannel.MethodCallHandler {
private val imageView: FImageView = FImageView(context)
private var methodChannel: MethodChannel
init {
//通信
methodChannel = MethodChannel(messenger, "FImageView_$id")
methodChannel.setMethodCallHandler(this)
if (args is Map<*, *>) {
imageView.setUrl(args["url"] as String)
}
print(args)
}
override fun getView(): View {
return imageView
}
override fun dispose() {
}
//通信flutter调用原生
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"setUrl" -> {
val url = call.argument<String>("url")
if (url != null) {
imageView.setUrl(url)
result.success("setUrl success")
} else {
result.error("-1", "url cannot be null", null)
}
}
else -> {
result.notImplemented()
}
}
}
}
上述代码我们通过实现PlatformView接口来创建了FImageViewController,当初始化FImageViewController时接受了一个图片URL参数,然后会通过给URL来加载图片
PlatformView接口主要有两个方法:
- View getView() : 用于返回Native View,这个View会被嵌入到Flutter的视图结构中
- void dispose() : 当Flutter要决定销毁PlatformView时会调用这个方法,通常在Flutter的一个widget销毁时调用,我们可以在这个方法中做一些资源释放的工作
准备PlatformViewFactory
class FImageViewFactory(private val messenger: BinaryMessenger) :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
return FImageViewController(context, messenger, viewId, args)
}
}
上述代码我们通过继承了PlatformViewFactory来创建了FImageViewFactory,PlatformViewFactory包含一个抽象方法
public abstract PlatformView create(Context context, int viewId, Object args);
该方法用来创建一个Native View,这个View会被嵌入到Flutter的视图结构中
注册Native UI
要想将Native UI 提供给Flutter使用,我们需要像Flutter注册Native UI,而注册Native UI的关键是要获取到注册器 (PluginRegistry.Registrar):
继承自FlutterActivity的场景下获取PluginRegistry.Registrar:
1.在Flutter v1.12以及以前版本中的FlutterActivity中通过 registrarFor("xxx")就可以获取
2.在Flutter v1.17以及以后的版本中只有 GeneratedPluginRegistrant 才能获取到,而 GeneratedPluginRegistrant 又是Flutter根据插件自动生成的,所以在这种场景下我们需要将 Native封装成插件然后发布到pub仓库并通过pub依赖来让Flutter自动完成注册配置自己创建 FlutterEngine 的场景,然后通过FlutterEngine来创建 ShiPluginRegistry,通过ShimpluginRegistry来获取PluginRegistry.Registrar,见下面的代码
当获取到 PluginRegistry.Registrar 之后我们就可以来准备 PlatformViewPlugin了
准备 PlatformViewPlugin
object FImageViewPlugin {
fun registerWith(flutterEngine: FlutterEngine){
val shimPluginRegistry = ShimPluginRegistry(flutterEngine)
registerWith(shimPluginRegistry.registrarFor("Flutter"))
}
fun registerWith(register:PluginRegistry.Registrar){
val fImageViewFactory = FImageViewFactory(register.messenger())
register.platformViewRegistry().registerViewFactory("FImageView",fImageViewFactory)
}
}
通过上述代码我们向Flutter注册了名为 FImageView 的Native View
上述代码需要在创建Flutter引擎的时候去注册(重要)
注册flutter / platform_views
当你的 FlutterEngine是自己手动创建的那么在默认情况下是无法在Flutter侧调用到所注册的Native UI的,在这种情况下使用Native UI 会抛出异常:
MissingPluginException(No implementation found method create on channel flutter/platform_views)
要想搞明白该问题,需要从Flutter的PlatformView的实现原理说起:Flutter之所以能够识别出来Native UI是因为有PlatforViewsChannel的存在,PlatforViewsChannel是一个借助 MethodChannel 来实现Flutter和Native间通信的插件,该插件的名字就叫: flutter/platform_views,主要用于PlatformView的通信.所以说要想使用Native UI 我们需要有PlatforViewsChannel,而每一个 PlatforViewsChannel 都与之对应的 PlatforViewsController,
PlatforViewsController 负责 PlatforViewsChannel的创建以及销毁等操作,查看PlatforViewsController的源码我们不难发现它有这样的一个方法:
public void attach(
Context context, TextureRegistry textureRegistry, @NonNull DartExecutor dartExecutor) {
if (this.context != null) {
throw new AssertionError(
"A PlatformViewsController can only be attached to a single output target.\n"
+ "attach was called while the PlatformViewsController was already attached.");
}
this.context = context;
this.textureRegistry = textureRegistry;
platformViewsChannel = new PlatformViewsChannel(dartExecutor);
platformViewsChannel.setPlatformViewsHandler(channelHandler);
}
在该方法中,PlatforViewsController创建了 PlatforViewsChannel
原理分析这么多,我们不难从中李处导致该问题的主要原因是,在这种自创建 FlutterEngine的情形下 PlatforViewsController 的 attach没有被调用,从而导致 PlatforViewsChannel 没有被创建.
那么为了解决该文件,我们不妨手动来调用 PlatforViewsController 的 attach方法:
flutterEngine?.platformViewsController?.attach(activity,flutterEngine.renderer,flutterEngine.dartExecutor)
Dart侧对应Widget封装
为了方便在Flutter中使用Native UI,我们可以在Dart侧来对 Native UI 做一次封装
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_module/native/f_image_view_controller.dart';
class FImageView extends StatefulWidget {
final String url;
const FImageView({Key key, this.url}) : super(key: key);
@override
_FImageViewState createState() => _FImageViewState();
}
class _FImageViewState extends State<FImageView> {
static const StandardMessageCodec _codec = StandardMessageCodec();
@override
Widget build(BuildContext context) {
return AndroidView(
viewType: "FImageView",
creationParams: {"url": widget.url},
creationParamsCodec: _codec,
);
}
}
在上述代码中我们通过 AndroidView然后指定他的 viewType来调用了我们在Android侧封装的名 FImageView 的 NativeUi