Understanding State Machines
Explains how state machines work in the dotLottie Android Player. Covers the core model, workflow, API, event posting, inputs, and debugging.
Understanding State Machines
This page explains how state machines work in the Android player — what they are, how they integrate with animation playback, and when to use them. It also documents the DotLottieController API for loading, starting, and interacting with state machines.
Note: State machine support is an actively developing feature. APIs and capabilities might change. If your state machine isn't working as expected please create an issue on Github↗.
Core Workflow
Define: Create your state machine logic (states, transitions, events, context) within your
.lottiefile.Load Animation: Set up the
DotLottieAnimationcomposable orDotLottieAnimationview, providing aDotLottieControllerinstance.Load State Machine: Once the animation is loaded, load the desired state machine by its ID using the controller.
Start State Machine: Activate the state machine using the controller.
Post Events: Send events from your application code (e.g., button clicks, view interactions) to trigger transitions using the controller.
Listen (Optional): Add a
StateMachineEventListenerto the controller to react to state changes or transitions.Stop (Optional): Stop the state machine using the controller when interactivity is no longer needed.
Loading on Initialization
You can load a state machine automatically when the animation loads by passing the stateMachineId parameter to the composable:
DotLottieAnimation(
source = DotLottieSource.Asset("interactive.lottie"),
autoplay = true,
stateMachineId = "main", // Loads and starts automatically
controller = controller,
)Android Player API for State Machines (Using DotLottieController)
You control state machines primarily through the DotLottieController instance associated with your DotLottieAnimation composable:
| Method | Return Type | Description |
stateMachineLoad(stateMachineId: String) | Boolean | Loads a specific state machine defined in the .lottie file by its ID. Returns true on success. |
stateMachineLoadData(data: String) | Boolean | Loads a state machine from a raw JSON string. Returns true on success. |
stateMachineStart(openUrl: OpenUrlPolicy, onOpenUrl: ((url: String) -> Unit)?) | Boolean | Starts the currently loaded state machine with URL policy configuration. Returns true on success. |
stateMachineStop() | Boolean | Stops the currently active state machine. Returns true on success. |
stateMachinePostEvent(event: Event) | Boolean | Sends a typed Event to the active state machine. Returns true if posted. |
stateMachinePostEvent(event: Event, force: Boolean) | Boolean | Sends an event with an option to force the post regardless of current state. |
stateMachineFire(event: String) | Boolean | Fires a named string event to the active state machine. |
stateMachineSetBooleanInput(key: String, value: Boolean) | Boolean | Sets a boolean input value on the state machine. |
stateMachineSetNumericInput(key: String, value: Float) | Boolean | Sets a numeric input value on the state machine. |
stateMachineSetStringInput(key: String, value: String) | Boolean | Sets a string input value on the state machine. |
stateMachineGetBooleanInput(key: String) | Boolean? | Gets the current value of a boolean input. |
stateMachineGetNumericInput(key: String) | Float? | Gets the current value of a numeric input. |
stateMachineGetStringInput(key: String) | String? | Gets the current value of a string input. |
stateMachineCurrentState() | String? | Returns the name of the currently active state. |
stateMachineAddEventListener(listener: StateMachineEventListener) | Unit | Adds a listener to receive state machine events. |
stateMachineRemoveEventListener(listener: StateMachineEventListener) | Unit | Removes a previously added state machine event listener. |
Important: Ensure the animation is loaded before calling stateMachineLoad. You can use the controller.isLoaded property or listen for the onLoad event via a DotLottieEventListener.
OpenUrlPolicy
The OpenUrlPolicy data class controls how URLs are handled when a state machine triggers a URL open action:
| Property | Type | Description |
requireUserInteraction | Boolean | Whether user interaction is required before opening URLs. |
whitelist | List<String> | List of allowed URL patterns. |
controller.stateMachineStart(
openUrl = OpenUrlPolicy(
requireUserInteraction = true,
whitelist = listOf("https://lottiefiles.com/*")
),
onOpenUrl = { url -> /* handle URL */ }
)Example: Exploding Pigeon (Jetpack Compose)
This example demonstrates loading, starting, posting events, and listening to a state machine.
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.lottiefiles.dotlottie.core.compose.ui.DotLottieAnimation
import com.lottiefiles.dotlottie.core.util.DotLottieSource
import com.lottiefiles.dotlottie.core.widget.DotLottieController
import com.lottiefiles.dotlottie.core.model.StateMachineEventListener
import com.lottiefiles.dotlottie.core.model.Event
import android.util.Log
@Composable
fun StateMachineExample() {
val controller = remember { DotLottieController() }
val stateListener = remember {
object : StateMachineEventListener {
override fun onStart() {
Log.i("DotLottie", "State machine started")
}
override fun onStop() {
Log.i("DotLottie", "State machine stopped")
}
override fun onStateEntered(enteringState: String) {
Log.i("DotLottie", "State Entered: $enteringState")
}
override fun onStateExit(leavingState: String) {
Log.i("DotLottie", "State Exited: $leavingState")
}
override fun onTransition(previousState: String, newState: String) {
Log.i("DotLottie", "Transition: $previousState -> $newState")
}
override fun onCustomEvent(message: String) {
Log.i("DotLottie", "Custom event: $message")
}
override fun onError(message: String) {
Log.e("DotLottie", "State machine error: $message")
}
override fun onInputFired(inputName: String) {
Log.i("DotLottie", "Input fired: $inputName")
}
override fun onBooleanInputValueChange(inputName: String, oldValue: Boolean, newValue: Boolean) {
Log.i("DotLottie", "Boolean input $inputName: $oldValue -> $newValue")
}
override fun onNumericInputValueChange(inputName: String, oldValue: Float, newValue: Float) {
Log.i("DotLottie", "Numeric input $inputName: $oldValue -> $newValue")
}
override fun onStringInputValueChange(inputName: String, oldValue: String, newValue: String) {
Log.i("DotLottie", "String input $inputName: $oldValue -> $newValue")
}
}
}
LaunchedEffect(controller) {
controller.stateMachineAddEventListener(stateListener)
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DotLottieAnimation(
source = DotLottieSource.Asset("exploding_pigeon.lottie"),
controller = controller,
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
val result = controller.stateMachineLoad("pigeon_fsm")
if (result) {
Log.i("DotLottie", "State machine loaded.")
if (controller.stateMachineStart(
openUrl = OpenUrlPolicy(requireUserInteraction = true, whitelist = emptyList()),
onOpenUrl = null
)
) {
Log.i("DotLottie", "State machine started.")
}
}
}) {
Text(text = "Load & Start")
}
Button(onClick = {
controller.stateMachinePostEvent(Event.Click(100f, 100f))
}) {
Text(text = "Click")
}
Button(onClick = {
controller.stateMachineStop()
}) {
Text(text = "Stop")
}
}
// Working with state machine inputs
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
controller.stateMachineSetBooleanInput("isActive", true)
}) {
Text(text = "Set Active")
}
Button(onClick = {
val currentState = controller.stateMachineCurrentState()
Log.i("DotLottie", "Current state: $currentState")
}) {
Text(text = "Log State")
}
}
}
}Exploding Pigeon State Machine Definition (in .lottie)
{
"descriptor": { "id": "pigeon_fsm", "initial": 0 },
"states": [
{
"name": "pigeon",
"type": "PlaybackState",
"marker": "bird",
"loop": true,
"autoplay": true,
"speed": 1
},
{
"name": "explosion",
"type": "PlaybackState",
"marker": "explosion",
"loop": false,
"autoplay": true,
"speed": 0.5
},
{
"name": "feathers",
"type": "PlaybackState",
"marker": "feathers",
"loop": false,
"autoplay": true,
"speed": 1
}
],
"transitions": [
{
"type": "Transition",
"from_state": 0,
"to_state": 1,
"string_event": { "value": "explosion" }
},
{
"type": "Transition",
"from_state": 1,
"to_state": 2,
"on_complete_event": {}
},
{
"type": "Transition",
"from_state": 2,
"to_state": 0,
"on_complete_event": {}
}
]
}Understanding State Machine Components (Defined in .lottie)
Understanding the structure defined within the .lottie file helps in using the player API effectively:
Descriptor: Contains the state machine's unique
id(used instateMachineLoad) and the index of theinitialstate.States: Defines possible animation states. The main type
PlaybackStatecontrols animation aspects likemarker,segment,loop,autoplay,speed, andmode.Transitions: Rules for moving between states (
from_state,to_state) based on a triggerevent.Events: Triggers for transitions, posted via the
stateMachinePostEventmethod using theEventsealed class.Guards (Optional): Conditions within transitions that check context variables to allow or prevent the transition.
Context Variables (Optional): Stored data (
Numeric,String,Boolean) used by guards.Listeners (Optional): Defined actions tied to events within the
.lottiefile (player execution support evolving).
Refer to the dotLottie format specification↗ or dotlottie-js documentation↗ for complete definition details.
Event Type for stateMachinePostEvent
Pointer and click interactions defined in the state machine's interactions array (e.g., Click, PointerDown, PointerEnter) are handled automatically by the Android player based on user touches on the animation view. You do not need to post these events manually for normal interactions.
The stateMachinePostEvent method is available for cases where you need to synthesize pointer events programmatically — for example, triggering an interaction from custom gesture logic outside the animation view.
The Android player uses a specific Event sealed class for posting events:
sealed class Event(val type: Int) {
data class PointerDown(val x: Float, val y: Float) : Event(0)
data class PointerUp(val x: Float, val y: Float) : Event(1)
data class PointerMove(val x: Float, val y: Float) : Event(2)
data class PointerEnter(val x: Float, val y: Float) : Event(3)
data class PointerExit(val x: Float, val y: Float) : Event(4)
data class Click(val x: Float, val y: Float) : Event(5)
object OnComplete : Event(6)
object OnLoopComplete : Event(7)
}Example:
controller.stateMachinePostEvent(Event.Click(100f, 200f))
controller.stateMachinePostEvent(Event.PointerDown(10f, 20f))
controller.stateMachinePostEvent(Event.PointerUp(10f, 20f))
controller.stateMachinePostEvent(Event.PointerMove(50f, 60f))
controller.stateMachinePostEvent(Event.OnComplete)
controller.stateMachinePostEvent(Event.OnLoopComplete)StateMachineEventListener Interface
The StateMachineEventListener interface defines all callbacks for state machine events:
| Method | Signature | Description |
onStart | fun onStart() | Fired when the state machine starts. |
onStop | fun onStop() | Fired when the state machine stops. |
onStateEntered | fun onStateEntered(enteringState: String) | Fired when entering a state. |
onStateExit | fun onStateExit(leavingState: String) | Fired when exiting a state. |
onTransition | fun onTransition(previousState: String, newState: String) | Fired on a state transition. |
onCustomEvent | fun onCustomEvent(message: String) | Fired when a custom event is emitted. |
onError | fun onError(message: String) | Fired when the state machine encounters an error. |
onInputFired | fun onInputFired(inputName: String) | Fired when an input fires. |
onBooleanInputValueChange | fun onBooleanInputValueChange(inputName: String, oldValue: Boolean, newValue: Boolean) | Fired when a boolean input changes. |
onNumericInputValueChange | fun onNumericInputValueChange(inputName: String, oldValue: Float, newValue: Float) | Fired when a numeric input changes. |
onStringInputValueChange | fun onStringInputValueChange(inputName: String, oldValue: String, newValue: String) | Fired when a string input changes. |
Transitions and Guards
Transitions and guards function conceptually the same as on other platforms. They are defined in the .lottie file and automatically evaluated by the player when an event is posted.
Context Variables
Context variables (Numeric, String, Boolean) are defined in the .lottie file and used primarily by guards. Currently, the Android player does not expose public methods to manually modify context variables at runtime. Context modification is expected to be handled via ListenerActions (a planned feature) defined within the state machine itself.
Debugging and Troubleshooting
State Machine Not Loading/Starting:
Check ID: Verify the
stateMachineIdpassed tostateMachineLoadmatches the ID in the.lottiemanifest.Check Return Values: Log the boolean results from
stateMachineLoadandstateMachineStart.Animation Loaded? Ensure the animation loaded correctly (check
controller.isLoadedor useDotLottieEventListener).
Events Not Triggering Transitions:
Check Event Type: Ensure you are using the correct
Eventsealed class case (e.g.,Event.Click(x, y),Event.PointerDown(x, y)).Verify Guards: Check guard conditions defined in the
.lottiefile.State Machine Started? Confirm
stateMachineStart()returnedtrue.
Animation Playback Issues in States:
Check Marker/Segment: Ensure
markernames orsegmentframe numbers defined inPlaybackStates exist in the animation JSON.
Debugging Tip: Use
StateMachineEventListener: Implement and add aStateMachineEventListener(as shown in the example) to log state entries, exits, transitions, and input changes in Logcat.
Known Limitations & Future Enhancements
Context Modification: No public API to manually change context variables.
Best Practices
Design: Keep state machines simple and document them within the
.lottiefile.Loading: Check return values from
stateMachineLoadandstateMachineStart.Events: Use the
Eventsealed class correctly with the current API (PointerDown,PointerUp,Click, etc.).Lifecycle: Consider adding/removing listeners (
StateMachineEventListener) in appropriate lifecycle scopes (e.g.,LaunchedEffect,rememberUpdatedState) to prevent leaks if the controller or listener instances change.Cleanup: Call
stateMachineStop()when the interactive feature is no longer needed.Debugging: Utilize
StateMachineEventListenerand Logcat.