State Machines Guide
State machines let you create interactive animations that respond to user input and application state. Instead of linear playback, state machines allow your animations to transition between different states based on inputs and events.
What are State Machines?
A state machine defines:
- States: Different animation states (e.g., “idle”, “hover”, “active”)
- Transitions: Rules for moving between states
- Inputs: Variables that control transitions (boolean, numeric, string)
Basic State Machine
Here’s a simple example of loading and controlling a state machine:
class StateMachineExample extends StatefulWidget { @override State<StateMachineExample> createState() => _StateMachineExampleState();}
class _StateMachineExampleState extends State<StateMachineExample> { DotLottieViewController? _controller; String _currentState = 'idle';
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('State Machine Example')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 300, height: 300, child: DotLottieView( source: 'https://lottie.host/your-animation.lottie', sourceType: 'url', stateMachineId: 'myStateMachine', onViewCreated: (controller) { _controller = controller; }, stateMachineOnStateEntered: (state) { setState(() { _currentState = state; }); }, ), ),
const SizedBox(height: 20),
Text('Current State: $_currentState'),
const SizedBox(height: 20),
ElevatedButton( onPressed: () { _controller?.stateMachineFire('buttonPressed'); }, child: const Text('Fire Event'), ), ], ), ), ); }}Setting Inputs
State machines can have four types of inputs:
Boolean Inputs
// Toggle a feature on/offElevatedButton( onPressed: () { _controller?.stateMachineSetBooleanInput('isActive', true); }, child: const Text('Activate'),)
ElevatedButton( onPressed: () { _controller?.stateMachineSetBooleanInput('isActive', false); }, child: const Text('Deactivate'),)Numeric Inputs
// Control speed or intensitySlider( value: _speed, min: 0.0, max: 10.0, onChanged: (value) { setState(() => _speed = value); _controller?.stateMachineSetNumericInput('speed', value); },)String Inputs
// Set a mode or categoryDropdownButton<String>( value: _mode, items: ['light', 'dark', 'auto'].map((mode) { return DropdownMenuItem( value: mode, child: Text(mode), ); }).toList(), onChanged: (mode) { if (mode != null) { setState(() => _mode = mode); _controller?.stateMachineSetStringInput('theme', mode); } },)Firing Events
Events cause immediate transitions:
// Fire an event_controller?.stateMachineFire('onTap');
// Multiple events for different interactionsGestureDetector( onTap: () => _controller?.stateMachineFire('tap'), onDoubleTap: () => _controller?.stateMachineFire('doubleTap'), onLongPress: () => _controller?.stateMachineFire('longPress'), child: AnimationWidget(),)Reading State Machine State
Get Current State
final currentState = await _controller?.stateMachineCurrentState();print('Current state: $currentState');Get Input Values
// Read boolean inputfinal isActive = await _controller?.stateMachineGetBooleanInput('isActive');
// Read numeric inputfinal speed = await _controller?.stateMachineGetNumericInput('speed');
// Read string inputfinal theme = await _controller?.stateMachineGetStringInput('theme');Get All Inputs
final inputs = await _controller?.stateMachineGetInputs();inputs?.forEach((name, type) { print('Input: $name (type: $type)');});State Machine Events
Listen to state machine events to react to state changes:
DotLottieView( source: 'assets/animation.lottie', sourceType: 'asset', stateMachineId: 'myStateMachine',
// State changes stateMachineOnStart: () { print('State machine started'); }, stateMachineOnStop: () { print('State machine stopped'); }, stateMachineOnStateEntered: (state) { print('Entered state: $state'); // Update UI based on state }, stateMachineOnStateExit: (state) { print('Exited state: $state'); }, stateMachineOnTransition: (previous, next) { print('Transition: $previous → $next'); },
// Input changes stateMachineOnBooleanInputValueChange: (name, oldValue, newValue) { print('$name: $oldValue → $newValue'); }, stateMachineOnNumericInputValueChange: (name, oldValue, newValue) { print('$name: $oldValue → $newValue'); },
// Events stateMachineOnInputFired: (name) { print('Event fired: $name'); },
// Errors and custom events stateMachineOnError: (message) { print('Error: $message'); }, stateMachineOnCustomEvent: (message) { print('Custom event: $message'); },)Complete Interactive Example
Here’s a complete example of an interactive button with multiple states:
class InteractiveButton extends StatefulWidget { @override State<InteractiveButton> createState() => _InteractiveButtonState();}
class _InteractiveButtonState extends State<InteractiveButton> { DotLottieViewController? _controller; String _currentState = 'idle'; bool _isEnabled = true;
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Interactive Button')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Interactive animation GestureDetector( onTapDown: (_) { if (_isEnabled) { _controller?.stateMachineFire('pressDown'); } }, onTapUp: (_) { if (_isEnabled) { _controller?.stateMachineFire('pressUp'); } }, onTapCancel: () { if (_isEnabled) { _controller?.stateMachineFire('pressCancel'); } }, child: SizedBox( width: 200, height: 200, child: DotLottieView( source: 'assets/button-animation.lottie', sourceType: 'asset', stateMachineId: 'buttonStateMachine', onViewCreated: (controller) { _controller = controller; }, stateMachineOnStateEntered: (state) { setState(() { _currentState = state; });
// Trigger haptic feedback on certain states if (state == 'pressed') { HapticFeedback.lightImpact(); } }, ), ), ),
const SizedBox(height: 40),
// State display Text( 'State: $_currentState', style: Theme.of(context).textTheme.titleLarge, ),
const SizedBox(height: 20),
// Control buttons Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () { setState(() => _isEnabled = !_isEnabled); _controller?.stateMachineSetBooleanInput( 'isEnabled', _isEnabled, ); }, child: Text(_isEnabled ? 'Disable' : 'Enable'), ), const SizedBox(width: 10), ElevatedButton( onPressed: () { _controller?.stateMachineFire('reset'); }, child: const Text('Reset'), ), ], ), ], ), ), ); }
@override void dispose() { _controller?.dispose(); super.dispose(); }}Loading State Machines Programmatically
You can also load state machines programmatically instead of using the stateMachineId property:
// Load by IDawait _controller?.stateMachineLoad('myStateMachine');await _controller?.stateMachineStart();
// Load from JSON datafinal stateMachineJson = '{"states":[...],"transitions":[...]}';await _controller?.stateMachineLoadData(stateMachineJson);await _controller?.stateMachineStart();
// Stop when doneawait _controller?.stateMachineStop();