我是一位经验丰富的 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。但是,我不知道该怎么做,我哪里出错了?
如果您想要两条不同的溪流,则需要使用两肘。你可以听第一肘中的第二肘,也许像这样
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);
}
}
这还有一个额外的好处,那就是能够在业务逻辑的其他部分监听这些流(因此是广播)。
然后我有两个单独的肘节用于将这些流呈现给 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'),
),
]);
});
}
}
现在效果很好,我们有很好的职责分工。