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

Flutter Cubit实战:如何有效拆分BLE设备的事件流?

问题:拆分Cubit模块,实现BLE跑步机连接与运动状态管理分离

背景

我是经验丰富的C#开发者,正在学习Flutter的Cubit功能。目前通过Flutter Blue获取BLE跑步机的事件流,能将设备发送的特征值字节映射为WorkoutStatus类实例。但当前的TreadmillControlService既要处理蓝牙连接管理,又要处理运动状态更新,我认为应该拆分为跑步机设备管理和运动状态管理两个独立模块。

现有方案与困惑

目前有两种思路:

  • 方案一:用TreadmillWorkoutUnion联合类耦合两者,虽然可行但不够优雅
  • 方案二:像C#里那样,把事件流拆成两个独立流,分别对应TreadmillState和WorkoutStatus的Cubit,但不知道具体怎么实现,想请教问题出在哪

相关代码

WorkoutStatus类

class WorkoutStatus {
  double speedInKmh;
  double distanceInKm;
  int timeInSeconds;
  int indicatedCalories;
  int steps;

  WorkoutStatus({
    required this.speedInKmh,
    required this.distanceInKm,
    required this.timeInSeconds,
    required this.indicatedCalories,
    required this.steps,
  });

  // 字节转实体的映射代码已省略
}

耦合的TreadmillControlService Cubit

class TreadmillControlService extends Cubit<TreadmillWorkoutUnion> {
  BluetoothDevice? _device;
  BluetoothCharacteristic? _control;
  BluetoothCharacteristic? _workoutStatus;

  // 用双下划线确保通过setter访问
  double __requestedSpeed = 0;

  WorkoutStatus? _status;
  static const double minSpeed = 1;
  static const double maxSpeed = 6;

  TreadmillControlService(super.initialState) {
    FlutterBluePlus.setLogLevel(LogLevel.warning, color: false);
  } 
  
  // 跑步机连接后调用此方法
  Future<void> _setupServices() async {
    await _device!.discoverServices();
    var fitnessMachine = _device!.servicesList.firstWhere((s) => s.uuid == Guid("1826"));
    _control = fitnessMachine.characteristics.firstWhere((c) => c.uuid == Guid("2ad9"));
    _workoutStatus = fitnessMachine.characteristics.firstWhere((c) => c.uuid == Guid("2acd"));
    _workoutStatus!.onValueReceived.listen(_processStatusUpdate);
    _workoutStatus!.setNotifyValue(true);
  }

  void _processStatusUpdate(List<int> value) {
    _status = WorkoutStatus.fromBytes(value);
    // 这里需要发送状态更新
  }

  // 以下方法实现已省略,因为无关
  Future<void> connect() async {  }
  Future<void> _wakeup() async {  }
  Future<void> start() async {  }
  Future<void> stop() async {  }
  Future<void> _setSpeed(double speed) async {  }
  void pause() {  }
  Future<void> speedUp() async {  }
  Future<void> speedDown() async {  }
  set _requestedSpeed(double value) {  }
  double get _requestedSpeed => __requestedSpeed;
}

TreadmillWorkoutUnion联合类

class TreadmillWorkoutUnion {
  TreadmillState treadmillState;
  WorkoutStatus workoutStatus;

  TreadmillWorkoutUnion(this.treadmillState, this.workoutStatus);
}

当前的状态处理逻辑

void _processStatusUpdate(List<int> value) {
  final workoutStatus = WorkoutStatus.fromBytes(value);
  // 可以考虑用工厂方法创建
  final treadmillState = TreadmillState(
      speedState: workoutStatus.speedInKmh == _requestedSpeed
          ? SpeedState.steady
          : workoutStatus.speedInKmh < _requestedSpeed
              ? SpeedState.increasing
              : SpeedState.decreasing,
      connectionState: _device!.isConnected ? ConnectionState.connected : ConnectionState.disconnected,
      requestedSpeed: _requestedSpeed,
      currentSpeed: workoutStatus.speedInKmh);
  emit(TreadmillWorkoutUnion(treadmillState, workoutStatus));
}

解决方案:拆分两个独立Cubit+依赖注入共享BLE事件流

核心思路是把原来的单一Cubit拆成两个职责单一的模块:

  1. TreadmillConnectionCubit:专门负责蓝牙连接、设备发现、服务初始化,同时暴露BLE的状态更新流
  2. WorkoutStatusCubit:专门处理运动状态的解析、更新,依赖前者提供的事件流

步骤1:定义各自的State

给两个Cubit分别定义独立的状态类:

// 跑步机连接状态枚举
enum ConnectionState { disconnected, connecting, connected, error }
enum SpeedState { steady, increasing, decreasing }

// 跑步机连接状态实体
class TreadmillConnectionState {
  final ConnectionState connectionState;
  final BluetoothCharacteristic? workoutStatusChar;
  final double requestedSpeed;
  final double currentSpeed;
  final SpeedState speedState;

  TreadmillConnectionState({
    required this.connectionState,
    this.workoutStatusChar,
    this.requestedSpeed = 0,
    this.currentSpeed = 0,
    this.speedState = SpeedState.steady,
  });

  // 拷贝方法,用于生成新状态
  TreadmillConnectionState copyWith({
    ConnectionState? connectionState,
    BluetoothCharacteristic? workoutStatusChar,
    double? requestedSpeed,
    double? currentSpeed,
    SpeedState? speedState,
  }) {
    return TreadmillConnectionState(
      connectionState: connectionState ?? this.connectionState,
      workoutStatusChar: workoutStatusChar ?? this.workoutStatusChar,
      requestedSpeed: requestedSpeed ?? this.requestedSpeed,
      currentSpeed: currentSpeed ?? this.currentSpeed,
      speedState: speedState ?? this.speedState,
    );
  }
}

// 运动状态包装类
class WorkoutStatusState {
  final WorkoutStatus? status;

  WorkoutStatusState(this.status);
}

步骤2:实现TreadmillConnectionCubit

这个Cubit只处理蓝牙相关逻辑,同时对外暴露运动状态特征值的流:

class TreadmillConnectionCubit extends Cubit<TreadmillConnectionState> {
  BluetoothDevice? _device;
  BluetoothCharacteristic? _controlChar;
  final _workoutStatusStreamController = StreamController<List<int>>.broadcast();
  
  // 对外暴露运动状态特征值的流
  Stream<List<int>> get workoutStatusStream => _workoutStatusStreamController.stream;

  static const double minSpeed = 1;
  static const double maxSpeed = 6;
  double _requestedSpeed = 0;

  TreadmillConnectionCubit() : super(TreadmillConnectionState(connectionState: ConnectionState.disconnected)) {
    FlutterBluePlus.setLogLevel(LogLevel.warning, color: false);
  }

  Future<void> connect() async {
    emit(state.copyWith(connectionState: ConnectionState.connecting));
    try {
      // 这里实现蓝牙设备搜索、连接逻辑,赋值给_device
      await _setupServices();
      emit(state.copyWith(connectionState: ConnectionState.connected));
    } catch (e) {
      emit(state.copyWith(connectionState: ConnectionState.error));
    }
  }

  Future<void> _setupServices() async {
    await _device!.discoverServices();
    var fitnessMachine = _device!.servicesList.firstWhere((s) => s.uuid == Guid("1826"));
    _controlChar = fitnessMachine.characteristics.firstWhere((c) => c.uuid == Guid("2ad9"));
    final workoutStatusChar = fitnessMachine.characteristics.firstWhere((c) => c.uuid == Guid("2acd"));
    
    // 监听特征值变化,转发到控制器
    workoutStatusChar.onValueReceived.listen((value) {
      _workoutStatusStreamController.add(value);
      // 更新连接状态里的速度相关字段
      final status = WorkoutStatus.fromBytes(value);
      final speedState = status.speedInKmh == _requestedSpeed
          ? SpeedState.steady
          : status.speedInKmh < _requestedSpeed
              ? SpeedState.increasing
              : SpeedState.decreasing;
      emit(state.copyWith(
        currentSpeed: status.speedInKmh,
        speedState: speedState,
      ));
    });
    await workoutStatusChar.setNotifyValue(true);
    emit(state.copyWith(workoutStatusChar: workoutStatusChar));
  }

  // 蓝牙控制方法:只负责发送BLE指令
  Future<void> speedUp() async {
    final newSpeed = _requestedSpeed + 1;
    if (newSpeed <= maxSpeed) {
      _requestedSpeed = newSpeed;
      await _setSpeed(newSpeed);
      emit(state.copyWith(requestedSpeed: newSpeed));
    }
  }

  Future<void> speedDown() async {
    final newSpeed = _requestedSpeed - 1;
    if (newSpeed >= minSpeed) {
      _requestedSpeed = newSpeed;
      await _setSpeed(newSpeed);
      emit(state.copyWith(requestedSpeed: newSpeed));
    }
  }

  Future<void> _setSpeed(double speed) async {
    // 实现发送速度指令到BLE设备的逻辑
  }

  @override
  Future<void> close() {
    _workoutStatusStreamController.close();
    return super.close();
  }
}

步骤3:实现WorkoutStatusCubit

这个Cubit依赖TreadmillConnectionCubit的流,只处理运动状态解析:

class WorkoutStatusCubit extends Cubit<WorkoutStatusState> {
  late final StreamSubscription<List<int>> _subscription;
  final TreadmillConnectionCubit _connectionCubit;

  WorkoutStatusCubit(this._connectionCubit) : super(WorkoutStatusState(null)) {
    // 订阅连接Cubit的运动状态流
    _subscription = _connectionCubit.workoutStatusStream.listen((value) {
      final status = WorkoutStatus.fromBytes(value);
      emit(WorkoutStatusState(status));
    });
  }

  @override
  Future<void> close() {
    _subscription.cancel();
    return super.close();
  }
}

步骤4:在UI层使用两个Cubit

通过MultiBlocProvider在Widget树中提供两个Cubit:

MultiBlocProvider(
  providers: [
    BlocProvider(create: (context) => TreadmillConnectionCubit()),
    BlocProvider(create: (context) => WorkoutStatusCubit(context.read<TreadmillConnectionCubit>())),
  ],
  child: YourTreadmillScreen(),
)

然后在UI中分别监听两个Cubit的状态:

// 监听连接状态
BlocBuilder<TreadmillConnectionCubit, TreadmillConnectionState>(
  builder: (context, state) {
    return Text('连接状态:${state.connectionState.name}');
  },
),
// 监听运动状态
BlocBuilder<WorkoutStatusCubit, WorkoutStatusState>(
  builder: (context, state) {
    final status = state.status;
    if (status == null) return Text('无运动数据');
    return Column(
      children: [
        Text('速度:${status.speedInKmh} km/h'),
        Text('距离:${status.distanceInKm} km'),
        // 其他运动数据展示
      ],
    );
  },
)

方案优势

  • 单一职责:每个Cubit只做一件事,逻辑清晰,易维护
  • 解耦:两者通过流通信,修改其中一个模块不会影响另一个
  • 贴合C#习惯:和你熟悉的C#事件流拆分思路一致,降低学习成本

内容的提问来源于stack exchange,提问作者ScottishTapWater

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 08:10:02