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/off
ElevatedButton(
onPressed: () {
_controller?.stateMachineSetBooleanInput('isActive', true);
},
child: const Text('Activate'),
)
ElevatedButton(
onPressed: () {
_controller?.stateMachineSetBooleanInput('isActive', false);
},
child: const Text('Deactivate'),
)

Numeric Inputs

// Control speed or intensity
Slider(
value: _speed,
min: 0.0,
max: 10.0,
onChanged: (value) {
setState(() => _speed = value);
_controller?.stateMachineSetNumericInput('speed', value);
},
)

String Inputs

// Set a mode or category
DropdownButton<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 interactions
GestureDetector(
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 input
final isActive = await _controller?.stateMachineGetBooleanInput('isActive');
// Read numeric input
final speed = await _controller?.stateMachineGetNumericInput('speed');
// Read string input
final 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 ID
await _controller?.stateMachineLoad('myStateMachine');
await _controller?.stateMachineStart();
// Load from JSON data
final stateMachineJson = '{"states":[...],"transitions":[...]}';
await _controller?.stateMachineLoadData(stateMachineJson);
await _controller?.stateMachineStart();
// Stop when done
await _controller?.stateMachineStop();