Как эффективно разделить поток событий с помощью Cubit

Я довольно опытный разработчик C#, пытающийся разобраться в функциональности локтя Flutter.

У меня есть поток событий от 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 должен делать гораздо больше, чем просто обрабатывать эти события, он также должен обрабатывать, например, соединение Bluetooth, и он должен иметь возможность указать обратно на уровень представления, каково состояние соединения. и т. д. Также существует разница между выбранной скоростью и фактической скоростью в определенные моменты времени во время линейного изменения. Я действительно думаю, что здесь происходят две разные вещи: управление самой беговой дорожкой и управление тренировкой (которая требует обновлений от беговой дорожки).

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 и создать Cubits отдельно для TreadmillState и WorkoutStatus. Однако я не понимаю, как это сделать, где я ошибаюсь?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
70
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вам нужно использовать два локтя, если вы хотите два разных потока. Второй локоть можно послушать в первом, возможно, вот так


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}",
    );
  }
}
Ответ принят как подходящий

Я хотел бы поблагодарить 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);
  }
}

Это дает дополнительное преимущество, заключающееся в возможности прослушивания этих потоков в других частях бизнес-логики (отсюда и трансляция).

Затем у меня есть два отдельных локтя для представления этих потоков в пользовательском интерфейсе:

Состояние беговой дорожки:

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, чтобы блок мог точно определять изменения состояния. Я не думаю, что это было бы необходимо для потока, исходящего от реальной службы управления беговой дорожкой, поскольку каждый раз создаются новые объекты. Однако у меня есть симуляция, которая изменяет состояние между выбросами.

Затем мне пришлось обернуть элементы управления в 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 для соответствующего Cubit:

Панель состояния тренировки:

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

Теперь это работает хорошо, и у нас хорошее разделение обязанностей.

Другие вопросы по теме