You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 06:27:18