state4d 0.0.1
state4d: ^0.0.1 copied to clipboard
Simple state management for Dart
import 'dart:math';
import 'package:result4d/result4d.dart';
import 'package:state4d/state4d.dart';
// this is our entity - it tracks the state
class CupOfTea {
final TeaState state;
final String lastAction;
CupOfTea(this.state, this.lastAction);
CupOfTea copyWith({TeaState? state, String? lastAction}) =>
CupOfTea(state ?? this.state, lastAction ?? this.lastAction);
@override
String toString() => 'CupOfTea(state: $state, lastAction: $lastAction)';
}
// the various states the entity can be in
enum TeaState {
getCup,
boilingWater,
steepingTea,
checkForMilk,
whiteTea,
blackTea,
}
// commands define actions which can result in dynamically generated events
enum TeaCommands { doYouHaveMilk }
// events transition the machine from one state to another
sealed class TeaEvent {}
class TurnOnKettle extends TeaEvent {}
class PourWater extends TeaEvent {}
class MilkPlease extends TeaEvent {}
class NoMilkPlease extends TeaEvent {}
class MilkIsFull extends TeaEvent {}
class MilkIsEmpty extends TeaEvent {}
// the lens gets and sets the state on the Entity
final lens = StateIdLens<CupOfTea, TeaState>(
(entity) => entity.state,
(entity, state) => entity.copyWith(state: state),
);
// commands is responsible for issuing new orders which will generate new events
Result<void, String> commands(CupOfTea entity, TeaCommands command) {
print('Issuing command $command for $entity');
return Success(null);
}
// define the machine - only Event type needed, rest inferred
final teaStateMachine = StateMachine.create(commands, lens).states<TeaEvent>(
(state) => [
// the state transitions for GetCup - we don't need to update the entity
state(TeaState.getCup).transition(TurnOnKettle, TeaState.boilingWater),
// the state transitions for BoilingWater - we can update the entity
state(TeaState.boilingWater).transition(
PourWater,
TeaState.steepingTea,
(event, entity) => entity.copyWith(lastAction: 'Waiting...'),
),
// when we enter SteepingTea, we ask if they have milk (a command). The result of that
// command will be a MilkPlease or NoMilkPlease event
state(TeaState.steepingTea)
.onEnter(TeaCommands.doYouHaveMilk)
.transition(MilkPlease, TeaState.checkForMilk)
.transition(NoMilkPlease, TeaState.blackTea),
state(TeaState.checkForMilk)
.transition(MilkIsFull, TeaState.whiteTea)
.transition(MilkIsEmpty, TeaState.blackTea),
state(TeaState.blackTea),
],
);
// this is the type of the result of a transition
typedef TeaResult =
Result<
StateTransitionResult<TeaState, CupOfTea, TeaEvent, TeaCommands>,
String
>;
void main() async {
// returns OK with the updated entity - state only,
final TeaResult boilingKettle = await teaStateMachine.transition(
CupOfTea(TeaState.getCup, '-'),
TurnOnKettle(),
);
final updatedCupOfTea = (boilingKettle as Success).value.entity;
print(updatedCupOfTea);
// returns OK with the updated entity - the lastAction is updated
final TeaResult steepingTea = await teaStateMachine.transition(
updatedCupOfTea,
PourWater(),
);
final updatedCupOfTea2 = (steepingTea as Success).value.entity;
print(updatedCupOfTea2);
// returns OK with the updated entity in state three or four
final TeaResult
blackOrCheckingForMilk = await teaStateMachine.transitionCommand(
updatedCupOfTea2,
TeaCommands.doYouHaveMilk,
(entity) async {
// imagine a remote operation here which could go one of 2 ways (or fail!)
return Random().nextBool()
? Success(NoMilkPlease())
: Success(MilkPlease());
},
);
print(blackOrCheckingForMilk);
// we can display the state machine as a Mermaid diagram
print(teaStateMachine.renderUsing(renderMermaid));
}