如何在Flutter Riverpod中正确控制StreamSubscription的启停
解决FlutterReactiveBle订阅无法停止/恢复的问题
问题场景
使用FlutterReactiveBle监听BLE特征值,通过Riverpod的StateProvider.family管理订阅逻辑,但点击按钮无法停止或恢复订阅,已创建的Socket始终未被取消,ListView仍持续接收数据。
问题根源
StateProvider.family特性导致重复创建订阅:每次调用ref.read(streamSubscriptionProvider(true/false))都会生成新的Provider实例,你取消/暂停的是刚创建的订阅,而最初的活跃订阅并未被引用和操作,因此会继续接收数据。StateProvider不适合管理资源生命周期:StateProvider设计用于存储简单状态,无法持久持有StreamSubscription这类需要手动管理生命周期的资源,容易出现资源泄漏和状态不一致。
修复方案
改用StateNotifierProvider管理订阅状态,通过StateNotifier持久保存订阅实例,统一处理启动、暂停、恢复和销毁逻辑。
1. 实现订阅管理的StateNotifier
import 'dart:async'; import 'dart:convert'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:riverpod/riverpod.dart'; // 定义订阅状态,保存订阅实例和激活状态 class BleSubscriptionState { final StreamSubscription? subscription; final bool isActive; BleSubscriptionState({this.subscription, this.isActive = false}); // 用于复制状态并更新属性 BleSubscriptionState copyWith({ StreamSubscription? subscription, bool? isActive, }) { return BleSubscriptionState( subscription: subscription ?? this.subscription, isActive: isActive ?? this.isActive, ); } } class BleSubscriptionNotifier extends StateNotifier<BleSubscriptionState> { final BluetoothService _bluetoothService; final Ref _ref; BleSubscriptionNotifier(this._bluetoothService, this._ref) : super(BleSubscriptionState()); // 切换订阅状态:启动/恢复 或 暂停 void toggleSubscription(bool shouldActivate) { if (shouldActivate) { if (state.subscription == null) { // 无订阅时创建新的 _createNewSubscription(); } else if (!state.isActive) { // 已有订阅但暂停时恢复 state.subscription?.resume(); state = state.copyWith(isActive: true); print("订阅已恢复"); } } else { // 暂停订阅(如需彻底取消,替换为cancel()并清空subscription) state.subscription?.pause(); state = state.copyWith(isActive: false); print("订阅已暂停"); } } // 创建新的BLE特征订阅 void _createNewSubscription() { final characteristicTX = QualifiedCharacteristic( serviceId: SERVICE_UUID, characteristicId: CHARACTERISTIC_TX_UUID, deviceId: "60:A4:23:C9:10:10", // 建议改为动态获取,避免硬编码 ); final subscription = _bluetoothService.flutterReactiveBle .subscribeToCharacteristic(characteristicTX) .listen((event) { // 更新数据流状态 final currentData = _ref.read(streamDataProvider.notifier).state; _ref.read(streamDataProvider.notifier).state = [ ...currentData, "${DateTime.now()} - ${utf8.decode(event)}", ]; }, onError: (error) { // 处理订阅错误,比如断开连接 print("订阅错误: $error"); state = BleSubscriptionState(subscription: null, isActive: false); }); state = BleSubscriptionState(subscription: subscription, isActive: true); print("新订阅已启动"); } @override void dispose() { // 销毁时取消订阅,防止内存泄漏 state.subscription?.cancel(); super.dispose(); } } // 对应的Provider final bleSubscriptionNotifierProvider = StateNotifierProvider<BleSubscriptionNotifier, BleSubscriptionState>((ref) { final bluetoothService = ref.watch(bluetoothServiceProvider); return BleSubscriptionNotifier(bluetoothService, ref); }); // 原有的其他Provider保持不变 final bluetoothServiceProvider = Provider<BluetoothService>((ref) { return BluetoothService(); }); final streamDataProvider = StateProvider<List<String>>((ref) { return []; });
2. 更新HomeView的按钮逻辑
return Scaffold( appBar: AppBar(title: const Text("APP")), body: Column( children: [ const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () { ref.read(bleSubscriptionNotifierProvider.notifier).toggleSubscription(true); }, child: const Text("启动/恢复订阅"), ), const SizedBox(width: 20), ElevatedButton( onPressed: () { ref.read(bleSubscriptionNotifierProvider.notifier).toggleSubscription(false); }, child: const Text("暂停订阅"), ), ], ), // 其他按钮和ListView保持不变 const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () async { await ref.read(bluetoothServiceProvider).getVersion(mine); }, child: const Text("Send command 'Get version'"), ), ], ), const SizedBox(height: 20), Expanded( child: Card( margin: const EdgeInsets.all(10), child: ListView.builder( padding: const EdgeInsets.all(10), itemCount: ref.watch(streamDataProvider).length, itemBuilder: (BuildContext context, int index) { return Text( ref.watch(streamDataProvider)[index], style: const TextStyle(fontSize: 8), ); }, ), ), ), ], ), );
关键优化点
- 持久持有订阅实例:通过
StateNotifier的状态保存StreamSubscription,确保所有操作针对同一个订阅。 - 生命周期管理:在
dispose方法中取消订阅,避免页面销毁后仍存在活跃订阅导致内存泄漏。 - 灵活控制逻辑:区分创建新订阅和恢复已有订阅的场景,避免重复订阅;可根据需求选择
pause()(可恢复)或cancel()(彻底终止)。 - 错误处理:添加订阅错误监听,在BLE断开等异常时重置状态。
内容的提问来源于stack exchange,提问作者Naografix
相关产品推荐
相关产品推荐

