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

如何在Flutter Riverpod中正确控制StreamSubscription的启停

解决FlutterReactiveBle订阅无法停止/恢复的问题

问题场景

使用FlutterReactiveBle监听BLE特征值,通过Riverpod的StateProvider.family管理订阅逻辑,但点击按钮无法停止或恢复订阅,已创建的Socket始终未被取消,ListView仍持续接收数据。

问题根源

  1. StateProvider.family特性导致重复创建订阅:每次调用ref.read(streamSubscriptionProvider(true/false))都会生成新的Provider实例,你取消/暂停的是刚创建的订阅,而最初的活跃订阅并未被引用和操作,因此会继续接收数据。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 15:08:20