跳至内容
返回

Flutter 嵌入安卓原生 View,以及与原生交互

发布于:  at  07:00 下午

在跨端开发里,有些场景是 Flutter 处理起来比较麻烦或者利用原生组件实现更高效。

这时候就得祭出 PlatformViewMethodChannel。不仅把一个 Android 原生 TextView 塞进了 Flutter 布局,还能实现Flutter和原生Android View的双向交互。

为了直观,我打算基于Flutter默认的计数器模板演示, 界面布局和悬浮按钮还是 Flutter 的,但中间显示的那个数字,换成安卓原生的TextView

Native Android TextView

把原生 View 嵌进来

Android 的 View 集成到 Flutter 的 Widget 树里的关键是使用 PlatformView。它能像常规 Widget 一样参与层级覆盖和事件分发。 (类似于Compose中的AndroidView, 不过因为跨语言的问题, Flutter的嵌入会比Compose复杂一些)

1. 实现原生渲染层 (Kotlin)

如果要把Android原生View嵌入Flutter, 就得为原生View做一个包装类, 继承自PlatformView。 并且需要实现getView()方法, 返回想要嵌入的View. 为了能让后续的 MethodChannel 找到这个 View 实例,我专门用一个 HashMap 来管理它们。 下面是用一个叫做NativeAndroidView`来演示, 他的构造函数内参数有三个, 分别是viewId, args, viewMaps.

**viewId:**这是由 Flutter 侧自动生成的唯一标识。当你页面上有多个相同的原生组件时,它是区分“谁是谁”的唯一凭证。

**args (CreationParams):**这是从 Dart 传过来的初始化“大礼包”。建议在这里处理那些只需设置一次的属性(如初始颜色、模式等),以减少后续 MethodChannel 的通信压力。

**viewMaps:**这是我们在插件层维护的“通讯录”。通过把 viewId 和实例绑定,我们才能在收到 Flutter 指令时,精准地找到对应的 TextView 进行更新。

// NativeAndroidView.kt
class NativeAndroidView(
    context: Context,
    messenger: BinaryMessenger,
    private val viewId: Int,
    args: Map<String, Any>?,
    private val viewMaps: HashMap<Int, PlatformView?>
) : PlatformView {

    private val textView = TextView(context)

    init {
        // 设置布局参数,虽然 PlatformView 往往会忽略 WrapContent
        textView.layoutParams = ViewGroup.LayoutParams(
            LayoutParams.WRAP_CONTENT,
            LayoutParams.WRAP_CONTENT
        )
        // 接受来自 Flutter 的初始参数
        textView.text = (args?.get("text") as? String).orEmpty()
        // 存入 Map 方便后续寻找
        viewMaps[viewId] = this
    }

    override fun getView(): View = textView

    override fun dispose() {
        viewMaps.remove(viewId)
    }
}

2. 衔接用的工厂类

Flutter得通过一个工厂类, 才能创建刚才定义的 View.

所以手动定义一个 NativeAndroidViewFactory 类, 继承自 PlatformViewFactory.

// NativeAndroidViewFactory.kt
class NativeAndroidViewFactory(
    private val messenger: BinaryMessenger,
    private val nativeViewMaps: HashMap<Int, PlatformView?>
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context?, viewId: Int, args: Any?): PlatformView {
        return NativeAndroidView(context!!, messenger, viewId, args as Map<String, Any>?, nativeViewMaps)
    }
}

逻辑通信:MethodChannel

构建View的工厂类和View都有了, 接下来就是如何让Flutter和原生View进行通信了.

提起跨端通信,难免想起以前 Hybrid 开发时监控 alert 来传消息的野路子。 而Flutter的跨端通信基于 MethodChannel,基于二进制消息,清爽且高效。

为了让代码更整洁,我把 View 注册和消息处理都封装在插件类(FlutterPlugin)里。注意看这里的 increment 分支,它通过 viewId 准确定位到了屏幕上的那个原生 View 实例并操作它。

// NativeAndroidViewPlugin.kt
class NativeAndroidViewPlugin : FlutterPlugin {
    private val nativeViewMaps = HashMap<Int, PlatformView?>()

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        val channel = MethodChannel(binding.binaryMessenger, "native_android_plugin")
        channel.setMethodCallHandler { call, result ->
            when (call.method) {
                "increment" -> {
                    val id = call.argument<Int>("viewId")
                    val text = call.argument<Int>("counter")
                    // 动态更新原生 View 的文字
                    nativeViewMaps[id]?.let { viewInstance ->
                        (viewInstance.view as TextView).text = text.toString()
                        result.success("0")
                    } ?: result.error("1", "View not found", null)
                }
            }
        }

        // 注册原生 UI 组件
        binding.platformViewRegistry.registerViewFactory(
            "plugins.flutter.io/native_android_view",
            NativeAndroidViewFactory(binding.binaryMessenger, nativeViewMaps)
        )
    }
}

最后, 别忘了在 MainActivity.kt 里把插件装上。


Flutter 这一头怎么接?

Flutter 这边就非常直白了, 繁琐的工作已经在前面做过了, 直接使用 AndroidView 再传递前面定义好的参数;想原生View进一步通信,就拿着 onPlatformViewCreated 回调里的 id借助MethodChannel去和原生通信.

// lib/main.dart

static const platform = MethodChannel('native_android_plugin');

Widget buildNativeView() {
    // 1. 平台安全检查
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(
        viewType: 'plugins.flutter.io/native_android_view',
        creationParams: const {'text': 'Android 原生 View'},
        creationParamsCodec: const StandardMessageCodec(),
        onPlatformViewCreated: (id) => _nativeViewId = id,
      );
    }

    // 2. 降级处理 (iOS 或其他平台)
    return const Center(
      child: Text('当前平台暂不支持此原生组件', style: TextStyle(color: Colors.grey)),
    );
  }

// 发送更新指令
platform.invokeMethod('increment', {
  'counter': _counter++,
  'viewId': _nativeViewId
});

PlatformView 是“贪婪”的

在折腾的过程中,可能发现 PlatformView 有个很显著的特性:它是贪婪的。, 他想尽可能占居所有空间.

在 Android 开发里,我们习惯用 WRAP_CONTENT 来让 View 自适应内容大小。但在 Flutter 的 PlatformView 体系下,这一招失效了。你会发现,即便你原生代码里写了自适应,这个原生 View 仍会强行铺满父布局给它的所有空间。

为什么会出现这种情况? 因为从渲染层面看,PlatformView 在 Flutter 的 Layer Tree 里其实是一个独立的合成层。Flutter 渲染引擎(比如新版的 Impeller)需要预先分配好一个固定大小的纹理或 Surface 来接收原生的渲染输出。它不像普通的 Flutter Widget 能够实时根据子插件的大小来动态收缩边界。

如何解决? 不要试图在 Android 原生侧去控制边界(改 LayoutParams 几乎没用)。你必须在 Flutter 侧给 AndroidView 套上一层约束,比如用 SizedBox 指定宽高,或者用 AspectRatio 锁定比例。只有在那一侧限制住“水池”的大小,里面的原生 View 才会乖乖服帖。


总结一下

底层视角:

PlatformView 虽好,但它本质上是在 Flutter 的渲染画布上“挖孔”。如果你熟悉 OpenGL,可以将其理解为:Flutter 与原生端共享 EGLContext,原生组件渲染后的结果直接写入指定的 textureID。对 Flutter 而言,它只负责消费这张纹理,而不参与其内部复杂的渲染指令。

适用场景:

重度原生 SDK: 地图、WebView、视频播放器等。 极高频动态更新: 如串口日志实时滚动刷新、频谱图等,这类场景利用原生 View 的缓存池和局部刷新机制更高效。

慎用场景:

简单的 UI 样式(圆角、阴影、动画): 能用 Flutter 自绘解决的,永远优先选择 Flutter。 **性能代价:**每一层 PlatformView 都会涉及跨线程的纹理提交和同步开销,过度使用会导致内存增长和明显的掉帧。


在以下平台分享此文章: