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

在跨端开发里,有些场景是 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 进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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.

1
2
3
4
5
6
7
8
9
10
// 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 实例并操作它。

  • FlutterPlugin 为我们提供onAttachedToEngineonDetachedFromEngine两个方法来感知Flutter中的生命周期.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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去和原生通信.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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 都会涉及跨线程的纹理提交和同步开销,过度使用会导致内存增长和明显的掉帧。

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

https://chaosgoo.com/flutter-deep-dive-platform-view-and-method-channel/

作者

Chaos Goo

发布于

2026-02-26

更新于

2026-02-26

许可协议