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





Вам нужно использовать два локтя, если вы хотите два разных потока. Второй локоть можно послушать в первом, возможно, вот так
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'),
),
]);
});
}
}
Теперь это работает хорошо, и у нас хорошее разделение обязанностей.