4、Android原生代码编写:在Android Studio中打开android目录、编写Java/Kotlin插件类、注册插件到FlutterEngine、处理来自Dart的调用

好,到了真正动手写原生代码的时候了。

前面我们搭好了环境,创建了项目。现在要做的,就是打开Android那边的大门,让Flutter和原生代码真正「握上手」。

我个人习惯先把Android Studio打开,直接定位到项目的android目录。你可能会问:为什么不用VS Code?嗯,写原生代码的时候,Android Studio的代码补全、布局预览、Gradle同步这些功能,确实比VS Code顺手太多。我早期踩过坑,用VS Code改原生代码,结果一个import没补全,编译报错找了半小时……后来就老实了。

4.1 打开android目录,找到正确的位置

在Flutter项目根目录下,有个android文件夹。用Android Studio打开它——注意,是单独打开这个文件夹,不是打开整个Flutter项目。

打开之后,你会看到熟悉的Android项目结构:

  • app/src/main/java/你的包名/ —— 这是主代码目录
  • app/build.gradle —— 模块级构建配置
  • settings.gradle —— 项目设置

我们要写的插件类,就放在你的包名下面。举个例子,如果包名是com.example.my_flutter_plugin,那路径就是:

android/app/src/main/java/com/example/my_flutter_plugin/

这里有个小细节:Flutter默认生成的MainActivity就在这个包下。我们的插件类,建议和MainActivity同级,或者单独建一个plugin包来管理。我个人习惯建一个plugin子包,这样代码结构更清晰。

4.2 编写Java/Kotlin插件类

现在开始写插件类。你可以用Java,也可以用Kotlin。我个人更推荐Kotlin,因为代码更简洁,空安全也更好。但如果你团队里Java老手多,用Java也没问题。

先看一个完整的Kotlin插件类示例:

package com.example.my_flutter_plugin

import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

class MyPlugin : MethodChannel.MethodCallHandler {

    companion object {
        private const val CHANNEL = "com.example.my_flutter_plugin/channel"

        fun registerWith(flutterEngine: FlutterEngine) {
            val channel = MethodChannel(
                flutterEngine.dartExecutor.binaryMessenger,
                CHANNEL
            )
            channel.setMethodCallHandler(MyPlugin())
        }
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "getPlatformVersion" -> {
                result.success("Android ${android.os.Build.VERSION.RELEASE}")
            }
            "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 {
        // 这里只是示例,实际需要Context
        return 85
    }
}

这段代码做了三件事:

  1. 实现了MethodChannel.MethodCallHandler接口
  2. 定义了一个静态的registerWith方法,用于注册
  3. onMethodCall里处理来自Dart的调用

注意那个CHANNEL常量。这个字符串必须和Dart端保持一致,否则两边对不上,调用会失败。我曾经在项目里因为大小写写错了一个字母,排查了整整一个下午……所以建议你把这个字符串定义成常量,两边引用,别手写。

4.3 注册插件到FlutterEngine

插件类写好了,怎么让它生效?

有两种方式:

  • 自动注册:通过Android的ContentProvider机制,在应用启动时自动注册。Flutter 2.0以后推荐这种方式。
  • 手动注册:在MainActivityApplication类里手动调用注册方法。

先看自动注册。你需要创建一个MyPluginRegistrar类,继承自FlutterPlugin

package com.example.my_flutter_plugin

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodChannel

class MyPluginRegistrar : FlutterPlugin {

    private lateinit var channel: MethodChannel

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(
            binding.binaryMessenger,
            "com.example.my_flutter_plugin/channel"
        )
        channel.setMethodCallHandler(MyPlugin())
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }
}

然后在AndroidManifest.xml里注册这个插件:

<provider
    android:name="io.flutter.plugins.GeneratedPluginRegistrant"
    android:authorities="${applicationId}.flutter_plugin_registrant"
    android:exported="false" />

嗯,其实Flutter 2.0以后,大部分插件都可以自动注册,你只需要在pubspec.yaml里依赖一下就行。但如果你需要手动控制注册时机,或者插件需要访问Activity生命周期,那就用手动注册。

手动注册的方式很简单,在MainActivity里:

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MyPlugin.registerWith(flutterEngine)
    }
}

我个人更推荐自动注册,因为少写代码,也少出错。但如果你需要插件在Application级别就初始化,那手动注册更灵活。

4.4 处理来自Dart的调用

现在Dart端发来一个调用,比如:

final result = await platform.invokeMethod('getBatteryLevel');

Android端收到后,会走到onMethodCall方法。这里有几个关键点:

  • 方法名匹配call.method就是Dart端传过来的方法名,必须完全一致。
  • 参数传递:Dart端可以传参数,通过call.arguments获取。可以是Map、List、String等基本类型。
  • 结果返回:通过result.success()返回成功结果,result.error()返回错误,result.notImplemented()表示方法未实现。

来看一个带参数处理的例子:

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
    when (call.method) {
        "calculateSum" -> {
            val args = call.arguments as Map<String, Int>
            val a = args["a"] ?: 0
            val b = args["b"] ?: 0
            result.success(a + b)
        }
        else -> result.notImplemented()
    }
}

这里要注意类型转换。Dart端传过来的参数,在Android端是Object类型,你需要手动转成正确的类型。如果类型不匹配,会抛出ClassCastException。我建议在转换前加个类型检查:

if (call.arguments is Map<*, *>) {
    // 安全转换
}

还有一个容易忽略的点:线程问题。默认情况下,onMethodCall是在主线程执行的。如果你的原生操作很耗时(比如网络请求、文件读写),一定要放到子线程去跑,否则会卡UI。我见过一个项目,因为直接在onMethodCall里做了图片压缩,导致界面卡顿了好几秒……

正确的做法是:

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
    when (call.method) {
        "doHeavyWork" -> {
            Thread {
                // 耗时操作
                val heavyResult = doSomethingSlow()
                // 回到主线程返回结果
                Handler(Looper.getMainLooper()).post {
                    result.success(heavyResult)
                }
            }.start()
        }
    }
}

核心要点总结

  • 插件类必须实现MethodChannel.MethodCallHandler
  • Channel名称在Dart和Android两端必须完全一致
  • 推荐使用自动注册方式(FlutterPlugin接口)
  • 耗时操作一定要放到子线程
  • 类型转换前做安全检查

小技巧

调试的时候,可以在onMethodCall开头加个日志:Log.d("MyPlugin", "收到调用: ${call.method}")。这样能快速定位是Dart没发过来,还是Android没处理到。

注意

如果你在插件里用到了Context(比如获取系统服务),记得通过FlutterPluginBindingapplicationContext获取,不要直接持有Activity的引用,否则可能造成内存泄漏。

Dart端 MethodChannel invokeMethod() 传递方法名+参数 调用 Flutter Engine BinaryMessenger 消息编解码 路由到插件 分发 Android原生 MethodCallHandler onMethodCall() 处理并返回结果 返回结果(success/error) 注册流程 自动注册:FlutterPlugin → onAttachedToEngine → 创建MethodChannel → setMethodCallHandler 手动注册:MainActivity.configureFlutterEngine → MyPlugin.registerWith(flutterEngine)

这张图展示了从Dart调用到Android原生处理,再返回结果的完整流程。你可以看到,Flutter Engine在这里扮演了「中间人」的角色,负责消息的编解码和路由。

写到这里,你应该已经掌握了Android原生插件开发的核心步骤。说白了就是三步:写插件类、注册到引擎、处理调用。但真正做项目的时候,你会发现细节远比想象的多——比如多线程、内存管理、生命周期等等。这些我们后面会慢慢展开。

专注资料整理