End

Flutter 陈航 26- 方法通道 通讯 MethodChannel 平台视图 PlatformView

本文地址


目录

26-MethodChannel

源码

方法通道解决了逻辑层原生能力复用问题,使得 Flutter 能够通过轻量级的异步方法调用,实现与原生代码的交互。

使用方法通道,我们可以把原生代码所拥有的能力,以接口形式提供给 Dart。这样,当发起方法调用时,Flutter 应用会以类似网络异步调用的方式,将请求数据通过一个唯一标识符指定的方法通道传输至原生代码宿主;而原生代码处理完毕后,会将响应结果通过方法通道回传至 Flutter,从而实现 Dart 代码与原生 Android、iOS 代码的交互。 这与调用一个本地的 Dart 异步 API 并无太多区别。

通过方法通道,我们可以把原生操作系统提供的底层能力,以及现有原生开发中一些相对成熟的解决方案,以接口封装的形式在 Dart 层快速搞定,从而解决原生代码在 Flutter 上的复用问题。然后,我们可以利用 Flutter 本身提供的丰富控件,做好 UI 渲染。

方法通道调用过程

一次典型的方法调用过程类似网络调用,由作为客户端的 Flutter,通过方法通道向作为服务端的原生代码宿主发送方法调用请求,原生代码宿主在监听到方法调用的消息后,调用平台相关的 API 来处理 Flutter 发起的请求,最后将处理完毕的结果通过方法通道回发至 Flutter。

调用过程如下图所示:

方法通道使用示例

Dart 发起调用请求

static const platform = MethodChannel('com.bqt.test/base_channel');

Future<String> _getBatteryLevel() async {
  try {
    final int result = await platform.invokeMethod('getBatteryLevel');
    return '电量 $result % .';
  } on PlatformException catch (e) {
    return "获取失败: '${e.message}'.";
  }
}

Android 响应请求

在 Android 平台处理和响应方法调用。

class TestMethodChannelActivity : FlutterActivity() {
    companion object {
        private const val CHANNEL_NAME = "com.bqt.test/base_channel"
        private const val METHOD_NAME = "getBatteryLevel"
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        val binaryMessenger: BinaryMessenger = flutterEngine.dartExecutor.binaryMessenger
        MethodChannel(binaryMessenger, CHANNEL_NAME).setMethodCallHandler { call, result ->
            when (call.method) {
                METHOD_NAME -> onCallGetBatteryLevel(result)
                else -> result.notImplemented()
            }
        }
    }

    private fun onCallGetBatteryLevel(result: MethodChannel.Result) {
        println("onCallGetBatteryLevel, isMainThread:${isMainThread()}") // true
        Thread {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
            if (batteryLevel != -1) {
                result.success(batteryLevel) // 可在子线程响应请求
                println("result callback, isMainThread:${isMainThread()}") // false
            } else {
                result.error("UNAVAILABLE", "Battery level not available.", null)
            }
        }.start()
    }
}

数据类型转换

在使用方法通道进行方法调用时,由于涉及到跨系统数据交互,Flutter 会使用 StandardMessageCodec 对通道中传输的信息进行序列化,以标准化数据传输行为。这样在我们发送或者接收数据时,这些数据就会根据各自系统预定的规则自动进行序列化和反序列化。

方法通道是非线程安全的

注意,方法通道是非线程安全的。

这意味着原生代码与 Flutter 之间所有接口调用必须发生在主线程

Flutter 是单线程模型,因此自然可以确保方法调用请求是发生在主线程(Isolate)的;而原生代码在处理方法调用请求时,如果涉及到异步或非主线程切换,需要确保回调过程是在原生系统的 UI 线程中执行的

27 | PlatformView 平台视图

方法通道解决的是原生能力逻辑复用问题,平台视图解决的就是原生视图复用问题。

平台视图

对于那些涉及到底层渲染,比如浏览器、相机、地图,以及原生自定义视图的场景,在 Flutter 上重新开发一套显然不太现实。在这种情况下,使用混合视图看起来是一个不错的选择。首先在 Flutter 的 Widget 树中提前预留一块空白区域,然后在 FlutterView 中嵌入一个与空白区域完全匹配的原生视图,就可以实现想要的视觉效果了。

但是,采用这种方案极其不优雅,因为嵌入的原生视图并不在 Flutter 的渲染层级中,需要同时在 Flutter 侧与原生侧做大量的适配工作,才能实现正常的用户交互体验。

为此,Flutter 提供了一个平台视图(Platform View)的概念,它提供了一种轻量级的方法,通过一些简单的接口封装,就可以将一个原生控件插入到 Widget 树中,然后就可以像使用普通 Widget 一样,实现原生视图与 Flutter 视图的混用。

平台视图使用示例

定义一个普通 Widget

class SampleView extends StatelessWidget {
  final int clickCount;
  static String viewType = "com.bqt.test/QtCustomView"; // 唯一标识符

  const SampleView({super.key, required this.clickCount});

  @override
  Widget build(BuildContext context) {
    return AndroidView(
      viewType: viewType,
      creationParams: "初始化参数:$clickCount", // 初始化参数,动态变更时不能通过这里传给 Android 端
      creationParamsCodec: const StandardMessageCodec(), // 初始化参数的编码
      onPlatformViewCreated: (id) => log("onPlatformViewCreated, id: $id"), // 原生视图创建完成后回调
    );
  }
}

视图工厂类 PlatformViewFactory

// 视图工厂类
class QtPlatformViewFactory(private val messenger: BinaryMessenger): PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, id: Int, obj: Any?) = QtPlatformView(context, id, obj)// 关联原生视图封装类
}

原生视图 PlatformView

// 原生视图封装类
class QtPlatformView(context: Context, id: Int, obj: Any?): PlatformView {

    private val qtView: View by lazy { QtCustomView(context, "$obj") } // 缓存原生视图

    override fun getView(): View = qtView // 返回原生视图
    override fun dispose() = Unit // 原生视图销毁回调
}

// 原生视图
class QtCustomView(context: Context, private val text: String): View(context) {
    private val qtPaint: Paint = Paint()

    init {
        qtPaint.color = Color.RED
        qtPaint.textSize = 50f
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawColor(Color.GREEN)
        canvas.drawText(text, 20f, qtPaint.textSize, qtPaint)
    }
}

建立绑定关系

private const val VIEW_TYPE_ID = "com.bqt.test/QtCustomView" // 唯一标识符

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    val messenger: BinaryMessenger = flutterEngine.dartExecutor.binaryMessenger
    val factory: PlatformViewFactory = QtPlatformViewFactory(messenger)
    flutterEngine.platformViewsController.registry.registerViewFactory(VIEW_TYPE_ID, factory) // 注册视图工厂
}

经过上面的封装与绑定,我们就可以在 Flutter 应用里,像使用普通 Widget 一样,去内嵌原生视图了。

SizedBox(width: 200, height: 200, child: SampleView(clickCount: _clickCount))

动态修改原生视图样式

在上面的例子中,我们将原生视图封装在一个 StatelessWidget 中,可以有效应对静态展示的场景。如果我们需要在程序运行时动态调整原生视图的样式,又该如何处理呢?

与基于声明式的 Flutter Widget,可以通过数据驱动其视图销毁重建不同,原生视图是基于命令式的,需要精确地控制视图展示样式。

为此,我们可以在原生视图的封装类中,将其持有的修改视图实例相关的接口,以方法通道的方式暴露给 Flutter,让 Flutter 也可以动态调整视图视觉样式。

原生视图的 onPlatformViewCreated 属性,会在其创建完成后以回调的形式通知视图 id,我们可以在这个时候注册方法通道,让后续的视图修改请求通过这条通道传递给原生视图。

总结

平台视图解决了原生渲染能力的复用问题,使得 Flutter 能够通过轻量级的代码封装,把原生视图组装成一个 Flutter 控件。

Flutter 提供了 平台视图工厂视图标识符 两个概念,因此 Dart 层发起的视图创建请求可以通过标识符直接找到对应的视图创建工厂,从而实现原生视图与 Flutter 视图的融合复用。对于需要在运行期动态调用原生视图接口的需求,我们可以在原生视图的封装类中注册 方法通道,实现精确控制原生视图展示的效果。

需要注意的是,由于 Flutter 与原生渲染方式完全不同,因此转换不同的渲染数据会有较大的性能开销。如果在一个界面上同时实例化多个原生控件,就会对性能造成非常大的影响,所以要避免在使用 Flutter 控件也能实现的情况下使用平台视图。

因为这样做,一方面需要分别在 Android 和 iOS 端写大量的适配桥接代码,违背了跨平台技术的本意,也增加了后续的维护成本;另一方面毕竟除去地图、WebView、相机等涉及底层方案的特殊情况外,大部分原生代码能够实现的 UI 效果,完全可以用 Flutter 实现。

2024-02-23

posted @ 2024-02-23 10:21  白乾涛  阅读(93)  评论(1编辑  收藏  举报