How to implement State Machines and Statecharts in dart and Flutter
Sandro Maglione
Get in touch with meMobile development
27 December 2023
•10 min read
Sandro Maglione
Mobile development
Learn how to implement type-safe State Machines and Statecharts in dart and Flutter:
- How to model a state machine in dart
- How to transition between states
- How to extend a state machine to add actions (Statechart)
- How to stream state changes (state management)
Modeling a State Machine in dart
State machines capture all the states, events and transitions between them.
Using state machines makes it easier to find impossible states and spot undesirable transitions.
Transitions are only allowed from valid states.
We define a state machine using an abstract class Machine
.
It has 2 key features:
state
: aString
variable to keep track of the machine's current stateevents
: define transitions between states. We use a nestedMap
: for each state (Map<String,
) defines a map of events (Map<String, Map<String,
) and the next state (Map<String, Map<String, String>>
)
A machine is then implemented by extending Machine
.
We set "Paused"
as the initial state and define a transition to the "Playing"
state when the "play"
event occurs:
Finally, we implement a add
method that takes an event
tries to find the next state based on the current state and the provided event.
If a valid next state exists, add updates the machine's internal state to reflect the change:
We can then create an instance of MyMachine
and send events to update the current state.
The key feature of state machines is that no state change is allowed when an event is sent from an invalid state
Generic types and type-safety
There is an issue with the previous implementation of Machine
. You can see it here below:
Found it? How do you spell "Paused"
? "Pasued"
?
The state transition has a typo! 🤯
These are the worst type of errors. Since we have no type-check and no code to report errors, this typo will remain unnoticed until everything breaks.
Then good luck finding the culprit 💁🏼♂️
This is where generic types can save us!
We introduce a new generic type parameter S
for states:
This is not enough. We want type safety also for events, otherwise:
We add another type parameter E
:
Now we can properly type the event
parameter in the add
method to completely remove any issue with possible typos:
💡 Important: Since in the
add
method we are checking for equality when extracting the next state fromMap
we need to make sure thatS
andE
implement the correct equality check.
Dart sealed class for type-safety
The next level of type safety comes with sealed
classes.
We enumerate all possible states and events using sealed
:
sealed
classes has been introduced in Dart 3.0.If you are interested in learning more about them you can read Records and Pattern Matching in dart - Complete Guide
We then use MyState
and MyEvent
as type parameters when creating MyMachine
:
In this way all states and all events are typed correctly. Any typo or incorrect use of a class becomes impossible at compile-time, since dart will report any issue ahead of time:
There is more.
Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.
Statecharts in dart
Statecharts extend traditional finite state machines to model more complex logic.
Statecharts add extra features not available in ordinary state machines: hierarchy, concurrency and communication.
We are going to extend Machine
by adding context and actions for states and events.
Machine context
We extend the machine by adding a context.
Context represent some internal state that can be updated from states and events after a transition (side-effects).
An example of context can be a form: while the
state
may be"Editing"
or"Sending"
, thecontext
is"username"
,"email"
,"password"
Since context can be any value we add it as another generic type C
:
Event action
In a state chart any event can trigger an action
(also called effect) that executes some code or/and updates context
.
An
EventAction
represents aFunction
that given the currentcontext
executes some side-effect and optionally returns an updatedcontext
An example of event action is playing an audio: when the "play"
event is sent the machine transitions from the Paused
to the Playing
state and an action will start the audio.
We want all events in the machine to have a possible action
.
We achieve this by defining an abstract
class Event
("contract"):
We then require all events to extend Event
:
State entry and exit actions
Actions can be triggered also when entering or exiting a state.
An example here may is entering a Reset
state: on enter we can trigger an action that updates context
to reset it to its initial value.
We implement entry and exit actions in the same way as events, by defining a new abstract
class State
:
We then require all states to extend State
:
Actions in machine transitions
The final step is executing the actions from the add
method.
We check if an action is defined and we trigger it, each time updating the current context.
Actions are executed in order: first the exit action, then the event action, and finally the entry action.
There is more.
Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.
Streaming new states
Inside Machine
states can change at any time in response to events.
We want to allow some part of the code to listen for state changes and react to them.
This is how state management works: some part of the code updates the state, and we need to notify all listeners of this change
We can achieve this in dart using Stream
.
dart:async
provides a StreamController
class that allows to broadcast state changes to multiple listeners. We therefore add an internal _stateController
inside Machine
:
Notice: We use the
.broadcast()
constructor to allow for multiple listeners, since by defaultStreamController
allows a single listener.
We then expose the Stream
of states from the controller (_stateController.stream
):
It is also important to define a close
method that allows to close the stream when the machine is no longer used:
Adding new states to the Stream
The last step is adding new states to the Stream
by calling add
from _stateController
:
In this way any part of our code can subscribe to state changes by calling listen
on streamState
:
This implementation is at the core of every state management solution in Flutter: some class contains the state and manages state updates by streaming changes
This is it!
Our Machine
implementation has all the features necessary to model state machines and state chart in dart.
We can then extend this solution for example by providing a Stream
also for context changes or by integrating Machine
with Flutter and widgets.
If you are interested to learn more, every week I publish a new open source project and share notes and lessons learned in my newsletter. You can subscribe here below 👇
Thanks for reading.