乐于分享
好东西不私藏

原生功能调不了?3步手写Flutter插件,Android/iOS双端一次搞定

原生功能调不了?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匹配方法名

EventChannelonListen/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

batteryStateDidChangeNotificationbatteryLevelDidChangeNotification两个通知都要监听

电池健康度在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,都得检查兼容性。但有些业务功能,真的只有自己能写。