State Trees topic
The StateTree
class represents a template for a state tree, defining the states in the tree and
their hierarchical relationship. There are several factories available to create a StateTree
, but
the simplest lists the available child states (which themselves may have children), and names the
state that will initially be active when the state machine starts.
StateTree(
InitialChild(States.unauthenticated),
childStates: [
// States go here
],
);
Naming States
Each state in a state tree is uniquely identified by a StateKey
. These keys are used when defining
states, and naming the destination state when a state transition occurs. For convenience, they are
often grouped together into a containing class. For example:
sealed class States {
static const unauthenticated = StateKey('unauthenticated');
static const authenticated = StateKey('authenticated');
// Use DataStateKey to identify a data state with an associated state data type
static const login = DataStateKey<LoginData>('login');
// ...
}
Defining States
States are declared using the State
or DataState
classes. These are created with a StateKey
that identifies the state, and callbacks that will be used to define how the state behaves. For
example, an onMessage
callback can be provided to specify the message handling behavior of the
state.
State(
States.unauthenticated,
onMessage: (MessageContext ctx) {
// Add message handling logic here
return ctx.unhandled();
},
);
See Message Handlers for more details on writing message handlers.
Child States
A state can be created with a collection of child states using the State.composite
factory, so
that the state becomes the parent of the the child states. If a child state is active, but does not
handle a message, the parent state will have an opportunity to handle it.
If a state has children, it must specify which of its child states to be enter when the parent is
entered using InitialChild
.
State.composite(
States.unauthenticated,
InitialChild(States.splash),
children: [
State(States.splash),
]
);
Data States
States often require additional data to model their behavior. For example, a 'counting' state might need an integer value to store the number of times an event occured. The data value(s) needed by a state are collectively referred to as state data (or more formally, extended state variables).
A data state can be defined using the DataState
class, providing the type of state data as a type
parameter. An InitialData
must be provided, indicating how to create the initial value of the
state data when the state is entered. Also note that a DataStateKey
, not a StateKey
, must be
used to identify a data state.
class LoginData {
String username = '';
String password = '';
String errorMessage = '';
}
sealed class States {
// Data states are identified by DataStateKeys
static const login = DataStateKey<LoginData>('login');
}
DataState(
States.login,
InitialData(() => LoginData()),
onMessage: (MessageContext ctx) {
// Use the MessageContext.data method to retrieve the current state
// data for a data state.
var loginData = ctx.data(States.login).value;
return ctx.unhandled();
}
);
The previous example illustates creating inital data when the initial value is known at design time.
Occasionly it may be necessary to defer creation until runtime, when the data state is being
entered. For example, the data may need to incorporate values obtained from the payload
of the
TransitionContext
. To handle this, use the InitialData.run
factory:
DataState(
States.login,
InitialData.run((TransitionContext transCtx) {
var userInfo = transCtx.payload as SavedUserInfo;
return LoginData()..email = userInfo.email;
}),
);
See Reading and writing state data to learn how to read and write state data when handling a message with a data state.
Final States
States may be delared as final states. Once a final state has been entered, no further message processing or state transitions will occur, and the state tree is considered ended, or complete. Note that a final state is always considered a child of the root state, and may not have any child states.
StateTree(
finalStates: [
FinalState(
States.lockedOut,
// An optional onEnter callback may be provided. onMessage and onExit are not
// available for final states.
onEnter: (TransitionContext ctx) {
// Add onEnter logic here
}
),
],
);
Machine States
Existing state trees or state machines can be composed with a second 'outer' state tree using a machine state. A machine state is a leaf state, and when it is entered an inner state machine will be started. The machine state will forward any messages from the outer state machine to the inner, and will remain the current state of the outer state machine until the inner reaches a final state. When it does so, the machine state will invoke a callback to determine the next state for the outer state machine to transition to.
StateTree innerStateTree() {
return StateTree(
// ...define a state tree
);
}
// Machine states need to be identified by MachineStateKey
final aMachineState = MachineStateKey('machineState');
final otherState = StateKey('otherState');
MachineState(
aMachineState,
// A nested state machine will be created from the state tree
InitialMachine.fromStateTree((TransactionContext ctx) => innerStateTree()),
onMachineDone: (MessageContext ctx, CurrentState currentInnerState) =>
// The inner state machine has completed, so determine the next state of the outer
// state machine.
ctx.goTo(otherState),
);
Classes
-
DataState<
D> State Trees Transition Handlers -
A data state with associated state data of type
D
. - State State Trees Transition Handlers
- A state in a state tree.
- StateTree Getting Started State Trees
- Defines a state tree that can be used in conjunction with a TreeStateMachine.