如何用Cubit有效划分事件流

问题描述 投票:0回答:2

我是一位经验丰富的 C# 开发人员,正在尝试了解 Flutter 的 cubit 功能。

我有一个来自 Flutter Blue 的事件流,每当设备发出某个 BLE 特性的更新时,它就会调用我的控制类上的一个方法。这很有用,我可以将有效负载字节映射到这个结构上:

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,
  });

  // Mapping code redacted for brevity
}

但是,您会注意到

TreadmillControlService
必须做的不仅仅是处理这些事件,它还必须处理蓝牙连接,例如,它需要能够向表示层指示返回什么连接状态等。在斜坡运行时,某些时候所选速度与实际速度之间也存在差异。我真的认为这里发生了两件不同的事情,管理跑步机本身和管理锻炼(需要跑步机的更新)。

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

  // Double underscore to ensure you use the 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);
  } 
  
  // This method gets called when the treadmill connects
  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);
    // This is where I need to emit the update
  }

  // Redacted all these bodies because they're irrelevant

  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;
}

所以在我看来,我有两个选择,我可以发出这两个东西的并集,并接受它们是耦合的:

class TreadmillWorkoutUnion {
  TreadmillState treadmillState;
  WorkoutStatus workoutStatus;

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

我不太喜欢(但确实有效)。

  void _processStatusUpdate(List<int> value) {
    final workoutStatus = WorkoutStatus.fromBytes(value);
    // Make a factory for this or something
    final treadmillSatus = 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(treadmillSatus, workoutStatus));
  }

或者,我想要做的以及我在 C# 中要做的就是在我的

_processStatusUpdate
方法中将其分成两个完全不同的事件流,并为
TreadmillState
WorkoutStatus
分别发出 Cubits。但是,我不知道该怎么做,我哪里出错了?

flutter dart bloc flutter-cubit
2个回答
0
投票

如果您想要两条不同的溪流,则需要使用两肘。你可以听第一肘中的第二肘,也许像这样


class WorkoutStatusCubit extends Cubit<WorkoutStatusState> {
  WorkoutStatusCubit() : super(WorkoutStatusState(0.0));

  void loadBytes(String speedText) {
    emit(WorkoutStatusState(double.parse(speedText)));
  }
}

class TreadmillControlService extends Cubit<TreadmillControlState> {
  TreadmillControlService(this.workoutStatusCubit)
      : super(TreadmillControlState(0.0)) {
    workoutStatusCubit.stream.listen(
      (workoutStatusState) {
        emit(
          calculateControlState(
              deviceInfo, workoutStatusState, _requestedSpeed),
        );
      },
    );
  }
}

这是完整的测试代码

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class WorkoutStatusState {
  double currentSpeed;
  DateTime lastUpdated;
  WorkoutStatusState(this.currentSpeed) : lastUpdated = DateTime.now();
}

class DeviceInfo {
  final String name = 'Treadmill';
}

class TreadmillControlState {
  TreadmillControlState(this.accerlation);
  final double accerlation;
}

class WorkoutStatusCubit extends Cubit<WorkoutStatusState> {
  WorkoutStatusCubit() : super(WorkoutStatusState(0.0));

  void loadBytes(String speedText) {
    emit(WorkoutStatusState(double.parse(speedText)));
  }
}

class TreadmillControlService extends Cubit<TreadmillControlState> {
  TreadmillControlService(this.workoutStatusCubit)
      : super(TreadmillControlState(0.0)) {
    workoutStatusCubit.stream.listen(
      (workoutStatusState) {
        emit(
          calculateControlState(
              deviceInfo, workoutStatusState, _requestedSpeed),
        );
      },
    );
  }

  DeviceInfo deviceInfo = DeviceInfo();
  WorkoutStatusCubit workoutStatusCubit;

  static TreadmillControlState calculateControlState(
    DeviceInfo deviceInfo,
    WorkoutStatusState workoutStatusState,
    double requestedSpeed,
  ) {
    if (requestedSpeed > workoutStatusState.currentSpeed) {
      return TreadmillControlState(1.0);
    } else if (requestedSpeed < workoutStatusState.currentSpeed) {
      return TreadmillControlState(-1.0);
    } else {
      return TreadmillControlState(0.0);
    }
  }

  double _requestedSpeed = 0.0;
  set requestedSpeed(double value) {
    _requestedSpeed = value;
    emit(calculateControlState(
        deviceInfo, workoutStatusCubit.state, _requestedSpeed));
  }

  double get requestedSpeed => _requestedSpeed;
}

class Device {
  Device(this.currentSpeed, this.controlState);

  double currentSpeed;
  TreadmillControlState controlState;

  String tick() {
    currentSpeed += controlState.accerlation;
    return "$currentSpeed";
  }
}

class MySimulation extends StatefulWidget {
  const MySimulation({super.key});

  @override
  State<MySimulation> createState() => _MySimulationState();
}

class _MySimulationState extends State<MySimulation> {
  late final Device device;
  late final WorkoutStatusCubit workoutStatusCubit;
  late final TreadmillControlService treadmillControlService;

  @override
  void initState() {
    super.initState();
    device = Device(0.0, TreadmillControlState(0.0));
    workoutStatusCubit = WorkoutStatusCubit();

    treadmillControlService = TreadmillControlService(workoutStatusCubit);

    Timer.periodic(Duration(seconds: 1), (timer) {
      workoutStatusCubit.loadBytes(device.tick());
    });
    treadmillControlService.stream.listen((TreadmillControlState data) {
      device.controlState = data;
    });
    treadmillControlService.requestedSpeed = 10.0;
  }

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(providers: [
      BlocProvider.value(value: workoutStatusCubit),
      BlocProvider.value(value: treadmillControlService)
    ], child: SimulartionView());
  }
}

class SimulartionView extends StatelessWidget {
  const SimulartionView({super.key});

  @override
  Widget build(BuildContext context) {
    final controlService = context.watch<TreadmillControlService>();
    final workoutStatusCubit = context.watch<WorkoutStatusCubit>();

    return Text(
      "Current Speed: ${workoutStatusCubit.state.currentSpeed} \n"
      "Control State: ${controlService.state.accerlation}",
    );
  }
}

0
投票

我要感谢 PurplePolyhedron 的回答,这让我朝着正确的方向前进,但并没有完全到达我想去的地方。


问题的关键似乎是我将肘位引入的水平太低了。在服务级别,我确实需要处理流本身,然后在需要桥接到表示层时引入 cubit。

为此,我首先将

TreadmillControlService
更改为将两个
StreamController
设置为
broadcast()
。一种用于跑步机状态更改,一种用于锻炼状态更改。在我特有的侦听器处理程序中,我可以分别写入这两个流。

// Irrelevant methods redacted for brevity
class TreadmillControlService implements Disposable {
  final StreamController<WorkoutStatus> _workoutStatusStreamController;
  final StreamController<TreadmillState> _treadmillStateStreamController;

  late Stream workoutStatusStream;
  late Stream treadmillStateStream;

  TreadmillControlService()
      : _workoutStatusStreamController = StreamController<WorkoutStatus>.broadcast(),
        _treadmillStateStreamController = StreamController<TreadmillState>.broadcast() {
    workoutStatusStream = _workoutStatusStreamController.stream;
    treadmillStateStream = _treadmillStateStreamController.stream;
    FlutterBluePlus.setLogLevel(LogLevel.warning, color: false);
  }

  void _processStatusUpdate(List<int> value) {
    final workoutStatus = WorkoutStatus.fromBytes(value);
    // Make a factory for this or something
    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);
    _workoutStatusStreamController.add(workoutStatus);
    _treadmillStateStreamController.add(treadmillState);
  }
}

这还有一个额外的好处,那就是能够在业务逻辑的其他部分监听这些流(因此是广播)。

然后我有两个单独的肘节用于将这些流呈现给 UI:

跑步机状态:

class TreadmillStateCubit extends Cubit<TreadmillState> {
  final TreadmillControlService _treadmillControlService;

  TreadmillStateCubit(this._treadmillControlService) : super(TreadmillState.initial()) {
    _treadmillControlService.treadmillStateStream.listen((state) {
      emit(state);
    });
  }

  void connect() => _treadmillControlService.connect();

  void start() => _treadmillControlService.start();

  void stop() => _treadmillControlService.stop();

  void speedUp() => _treadmillControlService.speedUp();

  void speedDown() => _treadmillControlService.speedDown();
}

锻炼状态:

class WorkoutStatusCubit extends Cubit<WorkoutStatus> {
  final TreadmillControlService _treadmillControlService;

  WorkoutStatusCubit(this._treadmillControlService) : super(WorkoutStatus.zero()) {
    _treadmillControlService.workoutStatusStream.listen((state) {
      emit(state);
    });
  }
}

我还必须在

Equatable
WorkoutStatus
上实现
TreadmillState
,以使 bloc 能够准确确定状态变化。我认为这对于来自真实跑步机控制服务的流来说是不必要的,因为每次都会创建新对象。但是,我有一个模拟可以改变发射之间的状态。

然后我不得不将控件包装在

MultiBlocProvider
:

class ControlPage extends StatelessWidget {
  final TreadmillControlService treadmillControlService;
  const ControlPage(this.treadmillControlService, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<WorkoutStatusCubit>(
          create: (ctx) => WorkoutStatusCubit(treadmillControlService),
        ),
        BlocProvider<TreadmillStateCubit>(
          create: (ctx) => TreadmillStateCubit(treadmillControlService),
        ),
      ],
      child: const Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          WorkoutStatusPanel(),
          TreadmillControls(),
        ],
      ),
    );
  }
}

并将相关子小部件移动到自己的类中,每个类都有一个

BlocBuilder
代表其相关的肘节:

锻炼状态面板:

class WorkoutStatusPanel extends StatelessWidget {
  const WorkoutStatusPanel({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<WorkoutStatusCubit, WorkoutStatus>(builder: (ctx, state) {
      return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
        Row(
          children: [
            UnitQuantityCard(state.timeInSeconds, "s", 0),
            UnitQuantityCard(state.distanceInKm, "km", 2),
          ],
        ),
        Row(
          children: [
            UnitQuantityCard(state.indicatedCalories, "kCal", 0),
            UnitQuantityCard(state.steps, "steps", 0),
          ],
        )
      ]);
    });
  }
}

跑步机控制:

class TreadmillControls extends StatelessWidget {
  const TreadmillControls({super.key});

  @override
  Widget build(BuildContext context) {
    final controlCubit = BlocProvider.of<TreadmillStateCubit>(context);
    return BlocBuilder<TreadmillStateCubit, TreadmillState>(builder: (ctx, state) {
      return Column(children: [
        Text("Connection: ${state.connectionState.name}"),
        Text("Speed ${state.speedState.name} (${state.currentSpeed}/${state.requestedSpeed})"),
        ElevatedButton(
          onPressed: () {
            controlCubit.connect();
          },
          child: const Text('Connect'),
        ),
        ElevatedButton(
          onPressed: () {
            controlCubit.start();
          },
          child: const Text('Start'),
        ),
        ElevatedButton(
          onPressed: () {
            controlCubit.stop();
          },
          child: const Text('Stop'),
        ),
        ElevatedButton(
          onPressed: () {
            controlCubit.speedUp();
          },
          child: const Text('Speed Up'),
        ),
        ElevatedButton(
          onPressed: () {
            controlCubit.speedDown();
          },
          child: const Text('Speed Down'),
        ),
      ]);
    });
  }
}

现在效果很好,我们有很好的职责分工。

© www.soinside.com 2019 - 2024. All rights reserved.