Flutter蓝牙应用如何通过状态管理跨页面共享BLE连接状态?
报错根因
context访问报错:你在State类的成员初始化位置调用Provider.of,此时Widget尚未完成挂载,context未初始化,无法访问。listen方法不存在报错:StreamProvider<T>对外暴露的是流推送的最新T类型实例,不是流本身。你注册的是StreamProvider<ConnectionStateUpdate>,因此Provider.of拿到的是单个连接状态对象,自然没有流的listen方法。- 原有
ReactiveProvider逻辑缺陷:getter中直接调用connectToAdvertisingDevice会在每次访问时重复创建连接流,且引用了未定义的_foundBleUARTDevices、index变量,无法正常运行。
可直接复用的实现方案
不要用单独的StreamProvider,把所有蓝牙相关的状态、操作逻辑全部封装到继承ChangeNotifier的全局状态类中,通过ChangeNotifierProvider在应用入口注册,全应用共享同一个实例即可。
第一步:编写蓝牙状态管理类
import 'package:flutter/foundation.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; class BleProvider extends ChangeNotifier { final flutterReactiveBle = FlutterReactiveBle(); List<DiscoveredDevice> foundBleUARTDevices = []; StreamSubscription<DiscoveredDevice>? _scanStream; Stream<ConnectionStateUpdate>? currentConnectionStream; StreamSubscription<ConnectionStateUpdate>? _connection; QualifiedCharacteristic? txCharacteristic; Stream<List<int>>? receivedDataStream; List<String> receivedData = []; String logTexts = ""; bool scanning = false; bool connected = false; int _numberOfMessagesReceived = 0; // 替换为你项目实际的UUID final Uuid _uartUuid = Uuid.parse("你的UART服务UUID"); final Uuid _uartRx = Uuid.parse("你的RX特征UUID"); final Uuid _uartTx = Uuid.parse("你的TX特征UUID"); void onNewReceivedData(List<int> data) { _numberOfMessagesReceived += 1; receivedData.add("$_numberOfMessagesReceived: ${String.fromCharCodes(data)}"); if (receivedData.length > 10) { receivedData.removeAt(0); } notifyListeners(); } Future<void> disconnect() async { await _connection?.cancel(); connected = false; notifyListeners(); } Future<void> stopScan() async { await _scanStream?.cancel(); scanning = false; notifyListeners(); } void startScan() async { foundBleUARTDevices = []; scanning = true; logTexts = ""; notifyListeners(); _scanStream = flutterReactiveBle .scanForDevices(withServices: [_uartUuid]).listen((device) { if (foundBleUARTDevices.every((element) => element.id != device.id)) { foundBleUARTDevices.add(device); notifyListeners(); } }, onError: (Object error) { logTexts = "${logTexts}ERROR while scanning:$error \n"; notifyListeners(); }, onDone: () async { await _scanStream?.cancel(); scanning = false; notifyListeners(); }); } void connectDevice(int index) { // 先断开已有连接 _connection?.cancel(); currentConnectionStream = flutterReactiveBle.connectToAdvertisingDevice( id: foundBleUARTDevices[index].id, prescanDuration: const Duration(seconds: 1), withServices: [_uartUuid, _uartRx, _uartTx], ); logTexts = ""; _connection = currentConnectionStream!.listen((event) { final id = event.deviceId.toString(); switch (event.connectionState) { case DeviceConnectionState.connecting: logTexts = "${logTexts}Connecting to $id\n"; break; case DeviceConnectionState.connected: connected = true; logTexts = "${logTexts}Connected to $id\n"; _numberOfMessagesReceived = 0; receivedData = []; txCharacteristic = QualifiedCharacteristic( serviceId: _uartUuid, characteristicId: _uartTx, deviceId: event.deviceId); receivedDataStream = flutterReactiveBle.subscribeToCharacteristic(txCharacteristic!); receivedDataStream!.listen((data) { onNewReceivedData(data); }, onError: (dynamic error) { logTexts = "${logTexts}Error:$error$id\n"; }); break; case DeviceConnectionState.disconnecting: connected = false; logTexts = "${logTexts}Disconnecting from $id\n"; break; case DeviceConnectionState.disconnected: connected = false; logTexts = "${logTexts}Disconnected from $id\n"; break; } notifyListeners(); }); } // 销毁时取消所有订阅,防止内存泄漏 @override void dispose() { _scanStream?.cancel(); _connection?.cancel(); super.dispose(); } }
第二步:应用入口注册全局Provider
void main() { runApp( MultiProvider(providers: [ ChangeNotifierProvider<BleProvider>( create: (context) => BleProvider(), ) ], child: const MainApp()), ); }
第三步:页面内调用方式
如果需要在initState阶段调用蓝牙方法(比如进入页面自动扫描),需要加listen: false参数,放在帧渲染回调中执行:
@override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final bleProvider = Provider.of<BleProvider>(context, listen: false); bleProvider.startScan(); }); } @override Widget build(BuildContext context) { // build方法中直接获取全局共享的蓝牙状态实例 final bleProvider = Provider.of<BleProvider>(context); return Scaffold( body: Column( children: [ Text("当前连接状态:${bleProvider.connected ? "已连接" : "未连接"}"), Expanded(child: ListView.builder( itemCount: bleProvider.foundBleUARTDevices.length, itemBuilder: (context, index) { return ListTile( title: Text(bleProvider.foundBleUARTDevices[index].name), onTap: () => bleProvider.connectDevice(index), ); }, )) ], ), floatingActionButton: FloatingActionButton( onPressed: () { bleProvider.scanning ? bleProvider.stopScan() : bleProvider.startScan(); }, child: Icon(bleProvider.scanning ? Icons.stop : Icons.search), ), ); }
蓝牙连接维持说明
所有蓝牙相关的流订阅、状态都存在全局唯一的BleProvider实例中,实例生命周期跟应用绑定,不会因为页面跳转、销毁被回收:
- 任意页面调用
connectDevice方法,都是操作全局同一份蓝牙连接实例,不会因为页面退出断开连接 - 任意页面通过
Provider.of<BleProvider>(context)即可拿到所有蓝牙相关参数(扫描列表、连接状态、接收数据、特征值等),不需要跨页面传参 - 全应用共享同一个连接状态,任意页面触发连接/断开操作,所有监听Provider的页面都会自动刷新最新状态。
内容的提问来源于stack exchange,提问作者Mubashar Ali
相关产品推荐
相关产品推荐

