You already know, Erlang is my favorite language... You should also probably know Erlang uses finite state machines as one of its core concept? In fact, it's probably why one can't come back from Erlang, when you know how to use them, everything becomes easier. This kind of pattern is not only available in Erlang, and few implementations exist on Dart. Here the interesting ones found on pub.dev and github:
| project | version | documentation | source code |
|---|---|---|---|
state_machine |
3.0.3 |
documentation | source code |
statemachine |
3.4.0 |
documentation | source code |
automata |
0.2.0 |
documentation | - |
dart_fsm |
1.3.1 |
documentation | source code |
immutable_fsm |
1.1.0 |
documentation | source code |
The most used of those project is state_machine with more than 22k download, then, this will be the first one to be tested in this article.
Bootstrapping
$ dart create fsm
Creating fsm using template console...
.gitignore
analysis_options.yaml
CHANGELOG.md
pubspec.yaml
README.md
bin/fsm.dart
lib/fsm.dart
test/fsm_test.dart
Running pub get... 1.1s
Resolving dependencies...
Downloading packages...
Changed 48 dependencies!
Created project fsm in fsm! In order to get started, run the following commands:
cd fsm
dart run
$ cd fsm
$ dart pub add state_machine
Resolving dependencies...
Downloading packages...
+ clock 1.1.2
+ intl 0.20.3
+ js 0.6.7 (0.7.2 available)
+ state_machine 3.0.3
+ w_common 4.0.0
Changed 5 dependencies!
1 package has newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.
The only required header to add in all your code requiring this module:
import 'package:state_machine/state_machine.dart';
Usage
The first step is to create a new StateMachine object. Only one argument is required for the constructor, the state machine name.
When a state machine changes its state, the onStateChange parameter is receiving the event, and then, react on it.
// create a new state machine
StateMachine fsm = StateMachine('fsm');
// react on state change with a closure
final onStateChange = (StateChange change) {
print('onStateChange: $change');
};
fsm.onStateChange.listen(onStateChange);
The second step is to create a new State objects representing respectively all accepted state by the state machine. To do that, the newState() method must be called. The only argument to pass is the name of the state.
During a transition, the state machine can enter into a new state or leave an old state. Those two kind of events can be followed by configuring respectively the onEnter and onLeave parameters.
// enter state closure
final onEnter = (StateChange change) {
print('onEnter: $change');
};
// leave state closure
final onLeave = (StateChange change) {
print('onLeave: $change');
};
// create a state called init
State init = fsm.newState('init');
init.onEnter.listen(onEnter);
init.onLeave.listen(onLeave);
// create a state called count
State count = fsm.newState('count');
count.onEnter.listen(onEnter);
count.onLeave.listen(onLeave);
// create a state called stop
State stop = fsm.newState('stop');
stop.onEnter.listen(onEnter);
stop.onLeave.listen(onLeave);
The transition between states can then be created. They are represented by StateTransition objects. This is the most "complex" method there, because the method newStateTransition requires 3 arguments. One is the name of the transition as a string, the second is the list of allowed states before the transition, and the last one is the next state after the transition.
StateTransition counting = fsm.newStateTransition('counting', [init,count], count);
StateTransition stopping = fsm.newStateTransition('stopping', [count], stop);
Then, the state machine can be started by calling the start() method. The only argument used here is a reference to the starting state.
fsm.start(init);
The transitions can now be called using the previous objects created. It is also possible to pass an argument to them as a payload.
// calling the counting transition
counting(1);
counting(2);
counting(3);
counting(4);
// calling the stopping transition
stopping();
When the state machine is terminated, one should use the dispose() method to clean up the memory and avoid memory leaks.
fsm.dispose();
The code looks okay. Let execute it to see what will happen.
$ dart run
...
onStateChange: StateChange: (none) --> init
onEnter: StateChange: (none) --> init
onStateChange: StateChange: init --> count
payload: 1
onLeave: StateChange: init --> count
payload: 1
onStateChange: StateChange: count --> count
payload: 2
onLeave: StateChange: count --> count
payload: 2
onStateChange: StateChange: count --> count
payload: 3
onLeave: StateChange: count --> count
payload: 3
onStateChange: StateChange: count --> count
payload: 4
onLeave: StateChange: count --> count
payload: 4
onStateChange: StateChange: count --> stop
onLeave: StateChange: count --> stop
onEnter: StateChange: count --> stop
onDispose: null
As you can see, the different steps are displayed on the screen, and we can see each state transitions. If an illegal transition happens in a specific state, the state machine throw an exception.
Conclusion
state_machine just works, but the way the state machine, the transitions and the events triggering the transitions are created annoys me a bit, perhaps because it's the OOP way to do it.
Indeed, to me, a great implementation of a state machine is from Erlang, with gen_statem, a state machine should embed its "data" or "state" and those elements should be modified during a transition.
Anyway, if you want to know more about the state_machine package, here few links:
state_machinepackage on pub.dev;state_machineofficial API documentation on pub.dev;state_machinesource code on Github;state_machineexample source code on Github;state_machinetest suite source code on Github.
Have fun and happy hacking!
Cover Image by Mike Hindle on Unsplash
Top comments (1)
The breakdown of using the
state_machinemodule in Dart is quite enlightening, especially how you've highlighted the simplicity of bootstrapping a finite state machine. It's interesting to see how theonStateChangelistener can effectively manage state transitions, which is crucial for building responsive applications. One potential improvement could be to explore integrating this with Dart’s built-in streams for more complex state handling scenarios. If you’re considering enhancements or new features for this state machine implementation, I’d be open to collaborating on that front!