原生功能调不了?3步手写Flutter插件,Android/iOS双端一次搞定
Flutter的pub.dev插件库确实丰富,但总有你找不到的功能。
咱们公司项目上周就遇到一个:需要读取设备的电池健康度(不仅是电量百分比,还要最大容量、充电状态等细节)。翻了半小时pub.dev,没有一个插件能满足咱们的精确需求。

这时候,等别人写插件不如自己来。
今天这篇文章,咱们就手把手写一个完整的Flutter原生插件,Android和iOS双端一次搞定。不用怕,没你想象的那么复杂。
为什么需要自己写插件?
Flutter虽然跨平台,但它本质上是个渲染引擎。涉及到系统级功能——蓝牙、传感器、电池信息、NFC——这些全得靠原生代码兜底。
Platform Channel就是Flutter和原生代码之间的桥梁。咱们这篇文章要做的就是:
1.在Flutter端定义好方法名和参数
2.在Android端用Kotlin实现
3.在iOS端用Swift实现
4.一套Flutter代码,双端自动调用
第一步:创建插件项目
别手动建文件,Flutter提供了脚手架:
●●●bash flutter create --template=plugin --platforms=android,ios \
--org com.yourcompany \
flutter_battery_info
这命令会生成一整套目录结构:
●●●code flutter_battery_info/
├── lib/
│ └── flutter_battery_info.dart # Dart API层
├── android/
│ └── src/main/kotlin/
│ └── FlutterBatteryInfoPlugin.kt # Android实现
├── ios/
│ └── Classes/
│ └── FlutterBatteryInfoPlugin.swift # iOS实现
├── example/ # 示例App
│ └── lib/main.dart
└── pubspec.yaml
example/目录很重要——调试插件全靠它。每次改完原生代码,跑cd example && flutter run就行。
第二步:定义Dart端API
打开lib/flutter_battery_info.dart,这是插件的门面:
●●●dart import'package:flutter/services.dart';
class FlutterBatteryInfo {
staticconst MethodChannel _channel =
MethodChannel('flutter_battery_info');
/// 获取电池信息Map
/// 返回: {level, isCharging, batteryHealth, maxCapacity}
static Future<Map<String, dynamic>> getBatteryInfo() async {
final Map<dynamic, dynamic> result =
await _channel.invokeMethod('getBatteryInfo');
return Map<String, dynamic>.from(result);
}
/// 监听充电状态变化
static Stream<Map<String, dynamic>> onBatteryChanged() {
const eventChannel =
EventChannel('flutter_battery_info/events');
return eventChannel.receiveBroadcastStream().map((event) {
return Map<String, dynamic>.from(event as Map<dynamic, dynamic>);
});
}
}
这里定义了两个东西:
●MethodChannel:一次性调用,拿当前电池状态
●EventChannel:持续监听,电池插拔时自动推送
注意channel名字flutter_battery_info要和原生端完全一致,一个字符都不能差。
第三步:Android端实现(Kotlin)
打开android/src/main/kotlin/.../FlutterBatteryInfoPlugin.kt:
●●●kotlin package com.yourcompany.flutter_battery_info
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class FlutterBatteryInfoPlugin : FlutterPlugin, MethodCallHandler,
EventChannel.StreamHandler {
private lateinit var channel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
privatevar eventSink: EventChannel.EventSink? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(
flutterPluginBinding.binaryMessenger,
"flutter_battery_info"
)
channel.setMethodCallHandler(this)
eventChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
"flutter_battery_info/events"
)
eventChannel.setStreamHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"getBatteryInfo" -> {
val bm = context.getSystemService(
Context.BATTERY_SERVICE
) as BatteryManager
val level = bm.getIntProperty(
BatteryManager.BATTERY_PROPERTY_CAPACITY
)
val isCharging = bm.isCharging
// 电池健康度(Android P+才支持)
val health = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
when (bm.getIntProperty(
BatteryManager.BATTERY_PROPERTY_HEALTH
)) {
BatteryManager.BATTERY_HEALTH_GOOD -> "good"
BatteryManager.BATTERY_HEALTH_OVERHEAT -> "overheat"
BatteryManager.BATTERY_HEALTH_DEAD -> "dead"
else -> "unknown"
}
} else {
"not_supported"
}
result.success(mapOf(
"level" to level,
"isCharging" to isCharging,
"batteryHealth" to health,
"maxCapacity" to -1// Android无法直接获取
))
}
else -> result.notImplemented()
}
}
// --- EventChannel 流式推送 ---
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
context.registerReceiver(batteryReceiver, filter)
}
override fun onCancel(arguments: Any?) {
eventSink = null
context.unregisterReceiver(batteryReceiver)
}
private val batteryReceiver = object : android.content.BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(
BatteryManager.EXTRA_STATUS, -1
)
val level = intent.getIntExtra(
BatteryManager.EXTRA_LEVEL, -1
)
val scale = intent.getIntExtra(
BatteryManager.EXTRA_SCALE, -1
)
val percent = if (level >= 0 && scale > 0) {
(level * 100 / scale)
} else -1
eventSink?.success(mapOf(
"level" to percent,
"isCharging" to status == BatteryManager.BATTERY_STATUS_CHARGING
|| status == BatteryManager.BATTERY_STATUS_FULL,
"batteryHealth" to "unknown",
"maxCapacity" to -1
))
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
}
}
几个关键点咱们拆开说:
●onAttachedToEngine:插件注册入口,在这里创建Channel
●onMethodCall:处理Flutter端的调用,call.method匹配方法名
●EventChannel的onListen/onCancel:管理广播接收器的生命周期
●版本兼容:Build.VERSION.SDK_INT判断,老版本给降级方案
第四步:iOS端实现(Swift)
打开ios/Classes/FlutterBatteryInfoPlugin.swift:
●●●swift import Flutter
import UIKit
publicclass SwiftFlutterBatteryInfoPlugin: NSObject,
FlutterPlugin, FlutterStreamHandler {
privatevar eventSink: FlutterEventSink?
publicstatic func register(with registrar: FlutterPluginRegistrar) {
// MethodChannel
let channel = FlutterMethodChannel(
name: "flutter_battery_info",
binaryMessenger: registrar.messenger()
)
let instance = SwiftFlutterBatteryInfoPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
// EventChannel
let eventChannel = FlutterEventChannel(
name: "flutter_battery_info/events",
binaryMessenger: registrar.messenger()
)
eventChannel.setStreamHandler(instance)
}
public func handle(_ call: FlutterMethodCall,
result: @escaping FlutterResult) {
guard call.method == "getBatteryInfo"else {
result(FlutterMethodNotImplemented)
return
}
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
let level = Int(device.batteryLevel * 100)
let isCharging = device.batteryState == .charging
|| device.batteryState == .full
// iOS没有公开API获取电池健康度
// 只能通过私有API,这里返回not_supported
let health = "not_supported"
let maxCapacity = -1
result([
"level": level,
"isCharging": isCharging,
"batteryHealth": health,
"maxCapacity": maxCapacity
])
}
// --- EventChannel ---
public func onListen(withArguments args: Any?,
eventSink events: @escaping FlutterEventSink)
-> FlutterError? {
self.eventSink = events
// 监听电池状态通知
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryChanged),
name: UIDevice.batteryStateDidChangeNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryChanged),
name: UIDevice.batteryLevelDidChangeNotification,
object: nil
)
UIDevice.current.isBatteryMonitoringEnabled = true
return nil
}
public func onCancel(withArguments args: Any?) -> FlutterError? {
eventSink = nil
NotificationCenter.default.removeObserver(
self, name: UIDevice.batteryStateDidChangeNotification, object: nil
)
NotificationCenter.default.removeObserver(
self, name: UIDDevice.batteryLevelDidChangeNotification, object: nil
)
return nil
}
@objc private func batteryChanged() {
let device = UIDevice.current
let level = Int(device.batteryLevel * 100)
let isCharging = device.batteryState == .charging
|| device.batteryState == .full
eventSink?([
"level": level,
"isCharging": isCharging,
"batteryHealth": "not_supported",
"maxCapacity": -1
])
}
}
iOS这边注意几点:
●必须先设isBatteryMonitoringEnabled = true,不然拿到的全是-1
●batteryStateDidChangeNotification和batteryLevelDidChangeNotification两个通知都要监听
●电池健康度在iOS上没有公开API,别想着用私有API过审
第五步:Flutter端调用
打开example/lib/main.dart,用起来就这么简单:
●●●dart import'package:flutter/material.dart';
import'package:flutter_battery_info/flutter_battery_info.dart';
voidmain() => runApp(constMyApp());
class MyApp extends StatefulWidget {
constMyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _info = '加载中...';
@override
voidinitState() {
super.initState();
_loadBatteryInfo();
_listenBattery();
}
Future<void> _loadBatteryInfo() async {
try {
final info = await FlutterBatteryInfo.getBatteryInfo();
setState(() {
_info = '''
电量: ${info['level']}%
充电中: ${info['isCharging']}
健康状态: ${info['batteryHealth']}
''';
});
} on PlatformException catch (e) {
setState(() => _info = '获取失败: ${e.message}');
}
}
void_listenBattery() {
FlutterBatteryInfo.onBatteryChanged().listen((info) {
setState(() {
_info = '''
电量: ${info['level']}%
充电中: ${info['isCharging']}
''';
});
});
}
@override
Widget build(BuildContext context) {
returnMaterialApp(
home: Scaffold(
appBar: AppBar(title: constText('电池信息')),
body: Center(child: Text(_info)),
),
);
}
}
运行cd example && flutter run,双端都能看到效果。
避坑指南
这几个坑咱们都踩过,提前说:
坑1:Channel名字不一致
Dart端写的'flutter_battery_info'和原生端必须一模一样。差一个大小写就报MissingPluginException。
坑2:iOS模拟器拿不到电池数据
模拟器没有真实电池,batteryLevel返回-1。测试必须用真机。
坑3:Android 14后台广播限制
Android 13+对后台广播有严格限制。如果你的插件需要在后台监听电池变化,得在AndroidManifest.xml里注册静态广播,并且加RECEIVE_EXPORTED权限:
●●●xml <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
坑4:Swift文件没注册
有时候新建了Swift文件但Xcode不认。进Xcode检查一下Build Phases → Compile Sources里有没有你的文件。
坑5:内存泄漏
EventChannel的onCancel一定要调用,不然广播接收器和NotificationCenter的observer都不会释放。插件detach的时候也要清理干净。
要不要自己写插件?决策建议
碰到原生功能,先按这个顺序来:
1.pub.dev上搜:有成熟插件(下载量>1000)直接用,别自己造轮子
2.看插件维护状态:最后一次更新超过1年、issue不回的,慎用
3.功能不匹配:差一两个参数?fork改一下比从头写快
4.完全找不到:那就上本文的流程,自己写
自己写插件的好处是可控,坏处是维护成本高——每次Flutter升级、iOS发新版SDK,都得检查兼容性。但有些业务功能,真的只有自己能写。
夜雨聆风