99、Flutter与Android混合开发:Flutter Module集成、MethodChannel、PlatformView

说实话,Flutter 和 Android 混合开发这个话题,我琢磨了挺久。

很多团队不是不想用 Flutter,而是老项目已经跑了好几年,不可能全盘重写。这时候,把 Flutter 作为一个模块嵌进去,就成了最务实的选择。我自己在几个中型项目里都这么干过,踩的坑不少,但最终效果确实不错。

Flutter Module 集成:把 Flutter 当“插件”用

先说说最基础的事——怎么把 Flutter 塞进现有的 Android 项目里。

官方推荐的方式是创建 Flutter Module,而不是 Flutter App。说白了,Module 就是给原生项目准备的“零件”。

核心思路:Flutter Module 编译后会生成 AAR 包,Android 项目直接依赖它就行。

具体步骤其实不复杂:

  1. 在 Android 项目同级目录下,用 flutter create --template module my_flutter 创建 Module。
  2. 在 Android 项目的 settings.gradle 里加上:setBinding(new Binding([gradle: this]));evaluate(new File(settingsDir, '../my_flutter/.android/include_flutter.groovy'))
  3. 然后在 app 的 build.gradle 里添加依赖:implementation project(':flutter')

嗯,这里要注意一点。如果你用的是 Android 原生工程,而不是 Flutter 创建的 Android 目录,那路径可能会对不上。我遇到过好几次,最后发现是 include_flutter.groovy 这个文件没找到。解决办法很简单——检查一下相对路径是否正确。

我的习惯:把 Flutter Module 放在 Android 项目的根目录下,这样路径最短,也最不容易出错。

MethodChannel:让 Dart 和 Java/Kotlin 说上话

Module 集成好了,接下来就是通信问题。Flutter 和原生之间怎么传数据?

MethodChannel 就是干这个的。它像一根管道,两头分别连着 Dart 和原生代码。

我画了一张图,帮你理解它的工作流程:

Dart 端 MethodChannel('com.example/channel') invokeMethod('getData') 原生端 (Android) MethodChannel(flutterView, 'com.example/channel') onMethodCall → 处理逻辑 MethodChannel 双向通信 Dart 调用示例 String result = await channel.invokeMethod('getBatteryLevel'); 原生处理示例 if (call.method == 'getBatteryLevel') result.success(batteryLevel)

你看,流程其实很清晰。Dart 端发一个请求,原生端收到后处理,再返回结果。

代码怎么写?我直接给你看核心部分。

Dart 端:

import 'package:flutter/services.dart';

class NativeBridge {
  static const MethodChannel _channel = MethodChannel('com.example/bridge');

  static Future<String> getBatteryLevel() async {
    try {
      final String result = await _channel.invokeMethod('getBatteryLevel');
      return result;
    } on PlatformException catch (e) {
      return "Failed: ${e.message}";
    }
  }
}

Android 端(Kotlin):

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example/bridge"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                if (call.method == "getBatteryLevel") {
                    val batteryLevel = getBatteryLevel()
                    if (batteryLevel != -1) {
                        result.success(batteryLevel)
                    } else {
                        result.error("UNAVAILABLE", "Battery level not available.", null)
                    }
                } else {
                    result.notImplemented()
                }
            }
    }

    private fun getBatteryLevel(): Int {
        val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    }
}

我曾经踩过的坑:MethodChannel 的名字一定要两边完全一致,大小写、标点都不能错。有一次我写成了 "com.example/bridge" 和 "com.example/Bridge",结果调试了一下午才发现是大小写问题。

PlatformView:把原生控件嵌入 Flutter

有些场景下,Flutter 自带的组件不够用。比如你要嵌入一个 WebView,或者用原生的地图 SDK。这时候就需要 PlatformView 了。

PlatformView 说白了,就是让 Flutter 的 Widget 树里,能直接放一个 Android 的 View。你想想看,这相当于在 Flutter 的画布上开了一个“窗口”,原生控件直接画在上面。

实现起来分两步:

  1. 在 Android 端创建一个 PlatformViewFactory
  2. 在 Flutter 端通过 AndroidViewUiKitView 来引用。

Android 端实现:

class NativeTextViewFactory : PlatformViewFactory {
    override fun create(context: Context, id: Int, args: Any?): PlatformView {
        return NativeTextView(context, id, args)
    }
}

class NativeTextView(context: Context, id: Int, args: Any?) : PlatformView {
    private val textView = TextView(context).apply {
        text = "我是原生 TextView"
        textSize = 24f
        setTextColor(Color.BLUE)
    }

    override fun getView(): View = textView

    override fun dispose() {}
}

然后在 MainActivity 里注册:

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    flutterEngine
        .platformViewsController
        .registry
        .registerViewFactory("native_text_view", NativeTextViewFactory())
}

Flutter 端使用:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('PlatformView 示例')),
    body: Center(
      child: AndroidView(
        viewType: 'native_text_view',
        layoutDirection: TextDirection.ltr,
        creationParams: null,
        creationParamsCodec: null,
      ),
    ),
  );
}

我的经验:PlatformView 在性能上会有一些损耗,因为 Flutter 和原生之间需要同步帧率。如果你的页面里 PlatformView 很多,建议做一下性能测试。我一般控制在 3 个以内,再多就容易掉帧。

三种方式的对比与选择

我把这三种方式整理成了一个表格,方便你对比:

方式 适用场景 通信方向 性能 复杂度
Flutter Module 整体嵌入 Flutter 页面 单向(原生→Flutter)
MethodChannel 数据传递、调用原生 API 双向
PlatformView 嵌入原生控件(地图、WebView) 单向(Flutter→原生) 中低

怎么选?其实很简单:

  • 如果你只是想把 Flutter 页面作为一个整体展示,用 Module 就够了。
  • 如果需要频繁传数据,比如获取设备信息、调用相机,那就用 MethodChannel。
  • 如果非要用原生控件不可,再考虑 PlatformView。

我个人习惯:能用 MethodChannel 解决的,尽量不用 PlatformView。因为 PlatformView 的维护成本确实高一些,尤其是不同 Android 版本上的兼容性问题。我曾经在 Android 10 上跑得好好的,换到 Android 12 就闪退,最后发现是 PlatformView 的触摸事件分发机制变了。

避坑指南

最后,分享几个我实际项目中遇到的坑:

  • MethodChannel 的线程问题:原生端的处理默认在 UI 线程,如果你要做耗时操作,记得开子线程,否则会卡界面。
  • PlatformView 的键盘问题:如果 PlatformView 里有输入框,键盘弹出时可能会遮挡。我当时的解决方案是监听键盘事件,手动调整布局。
  • Flutter Module 的版本同步:Flutter 和 Android 的 Gradle 版本要匹配,否则编译会报错。建议用 Flutter 官方推荐的版本组合。

嗯,差不多就这些了。混合开发这条路,说难不难,说简单也不简单。关键是理解清楚每种方式的定位,然后根据实际场景去选。你动手试试看,遇到问题再回来翻翻这篇文章,应该能解决大部分疑惑。


公众号:蓝海资料掘金营,微信deep3321