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
}
}
这段代码做了三件事:
- 实现了
MethodChannel.MethodCallHandler接口 - 定义了一个静态的
registerWith方法,用于注册 - 在
onMethodCall里处理来自Dart的调用
注意那个CHANNEL常量。这个字符串必须和Dart端保持一致,否则两边对不上,调用会失败。我曾经在项目里因为大小写写错了一个字母,排查了整整一个下午……所以建议你把这个字符串定义成常量,两边引用,别手写。
4.3 注册插件到FlutterEngine
插件类写好了,怎么让它生效?
有两种方式:
- 自动注册:通过Android的
ContentProvider机制,在应用启动时自动注册。Flutter 2.0以后推荐这种方式。 - 手动注册:在
MainActivity或Application类里手动调用注册方法。
先看自动注册。你需要创建一个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(比如获取系统服务),记得通过FlutterPluginBinding的applicationContext获取,不要直接持有Activity的引用,否则可能造成内存泄漏。
这张图展示了从Dart调用到Android原生处理,再返回结果的完整流程。你可以看到,Flutter Engine在这里扮演了「中间人」的角色,负责消息的编解码和路由。
写到这里,你应该已经掌握了Android原生插件开发的核心步骤。说白了就是三步:写插件类、注册到引擎、处理调用。但真正做项目的时候,你会发现细节远比想象的多——比如多线程、内存管理、生命周期等等。这些我们后面会慢慢展开。