dotLottie Android Player Usage Guide
Comprehensive guide for the dotLottie Android player. Covers loading sources, styling, event handling, playback control, slots, layout, multi-animation, and theming.
dotLottie Android Player Usage Guide
This guide shows you how to load animations, control playback, handle events, customize slots, configure layout, and manage themes in your Android app using the DotLottieAnimation composable.
Basic Usage
Common use cases for the DotLottieAnimation composable function include loading and displaying dotLottie animations within Jetpack Compose UIs. The following example demonstrates how to integrate a dotLottie animation into a Composable function using the DotLottieAnimation composable function.
Loading from different sources
From a URL
DotLottieSource.Url is used to load an animation from a URL. It can load .json or .lottie files from a URL.
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.lottiefiles.dotlottie.core.compose.ui.DotLottieAnimation
import com.lottiefiles.dotlottie.core.util.DotLottieSource
@Preview()
@Composable
fun DotLottiePreview() {
DotLottieAnimation(
source = DotLottieSource.Url("https://lottie.host/5525262b-4e57-4f0a-8103-cfdaa7c8969e/VCYIkooYX8.json"),
autoplay = true,
loop = true,
)
}From an asset
DotLottieSource.Asset is used to load an animation from the assets folder in your Android project. It can load .json or .lottie files from the assets folder.
// imports
@Preview()
@Composable
fun DotLottiePreview() {
DotLottieAnimation(
source = DotLottieSource.Asset("file.json"),
)
}From a raw resource
DotLottieSource.Res is used to load an animation from the res/raw directory. Pass the resource ID directly.
// imports
@Preview()
@Composable
fun DotLottiePreview() {
DotLottieAnimation(
source = DotLottieSource.Res(R.raw.animation),
autoplay = true,
loop = true,
)
}From raw data
DotLottieSource.Data is used to load an animation from raw data. It can load .lottie files from raw data.
// imports
@Preview()
@Composable
fun DotLottiePreview() {
DotLottieAnimation(
source = DotLottieSource.Data(byteArrayOf(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09),
)
}From a JSON string
DotLottieSource.Json is used to load an animation from a JSON string. It can load .json files from a JSON string.
// imports
@Preview()
@Composable
fun DotLottiePreview() {
DotLottieAnimation(
source = DotLottieSource.Json("{ \"v\": \"5.5.4\", \"fr\": 30, \"ip\": 0, \"op\": 180, \"w\": 1920, \"h\": 1080, \"nm\": \"Lottie Animation\" .... }"),
)
}Changing background color
Using the Modifier parameter, you can change the background color of the DotLottieAnimation composable function.
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
// other imports
@Preview()
@Composable
fun DotLottiePreview() {
DotLottieAnimation(
...,
modifier = Modifier.background(Color.LightGray)
)
}Adjusting animation display size
If you want to control the animation size, you can use the Modifier parameter to set the size. You can use the width,height or size modifier to set the size of the DotLottieAnimation composable function. fillMaxWidth(), fillMaxSize(), etc. can also be used to control the size of the animation.
import androidx.compose.ui.Modifier
// other imports
@Preview()
@Composable
fun DotLottiePreview() {
DotLottieAnimation(
...,
modifier = Modifier.size(200.dp)
)
}Event Handling
You can listen to various animation lifecycle events by providing a list of DotLottieEventListener implementations to the eventListeners parameter.
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.lottiefiles.dotlottie.core.compose.ui.DotLottieAnimation
import com.lottiefiles.dotlottie.core.util.DotLottieSource
import com.lottiefiles.dotlottie.core.model.DotLottieEventListener
import android.util.Log
// Example Listener Implementation
class MyEventListener : DotLottieEventListener {
override fun onLoad() { Log.d("DotLottieEvent", "onLoad") }
override fun onPlay() { Log.d("DotLottieEvent", "onPlay") }
override fun onPause() { Log.d("DotLottieEvent", "onPause") }
override fun onStop() { Log.d("DotLottieEvent", "onStop") }
override fun onFrame(frame: Float) { /* Log.d("DotLottieEvent", "onFrame: $frame") */ }
override fun onLoop(loopCount: Int) { Log.d("DotLottieEvent", "onLoop: $loopCount") }
override fun onComplete() { Log.d("DotLottieEvent", "onComplete") }
override fun onRender(frameNo: Float) { /* Log.d("DotLottieEvent", "onRender: $frameNo") */ }
override fun onLoadError(error: Throwable) { Log.e("DotLottieEvent", "onLoadError", error) }
override fun onError(error: Throwable) { Log.e("DotLottieEvent", "onError", error) }
override fun onFreeze() { Log.d("DotLottieEvent", "onFreeze") }
override fun onUnFreeze() { Log.d("DotLottieEvent", "onUnFreeze") }
override fun onDestroy() { Log.d("DotLottieEvent", "onDestroy") }
}
@Preview
@Composable
fun EventListenerPreview() {
val listener = remember { MyEventListener() } // Remember listener instance
DotLottieAnimation(
source = DotLottieSource.Url("https://lottie.host/5525262b-4e57-4f0a-8103-cfdaa7c8969e/VCYIkooYX8.json"),
autoplay = true,
loop = false, // Set loop=false for onComplete to trigger naturally
eventListeners = listOf(listener) // Pass the listener instance
)
}Note: Implement only the methods you need. The DotLottieEventListener interface provides default empty implementations for all methods.
Playback Control with DotLottieController
For fine-grained control over playback (play, pause, speed, seeking, etc.), use a DotLottieController.
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectTapGestures
// other imports from previous example...
import com.dotlottie.dlplayer.Mode
import com.lottiefiles.dotlottie.core.widget.DotLottieController
@Preview
@Composable
fun ControllerPreview() {
val controller = remember { DotLottieController() }
// Example: Read state from controller
val isPlaying by controller.isPlaying.collectAsState()
val currentFrame by controller.currentFrame.collectAsState()
Surface {
Column {
DotLottieAnimation(
source = DotLottieSource.Url("https://lottie.host/294b684d-d6b4-4116-ab35-85ef566d4379/VkGHcqcMUI.lottie"),
autoplay = false,
loop = true,
controller = controller, // Assign the controller
modifier = Modifier.pointerInput(Unit) { // Example: Play/Pause on tap
detectTapGestures(
onPress = {
if (controller.isPlaying.value) {
controller.pause()
} else {
controller.play()
}
}
)
}
)
Text("Playing: $isPlaying, Frame: ${currentFrame.toInt()}")
Button(onClick = { controller.setSpeed(2f) }) {
Text("Speed 2x")
}
Button(onClick = { controller.setPlayMode(Mode.REVERSE) }) {
Text("Reverse")
}
Button(onClick = { controller.setFrame(50f) }) {
Text("Go to Frame 50")
}
}
}
}Note: The DotLottieController also provides properties like isLoaded, isPaused, totalFrames, duration, currentState, width, height, markers, and stateMachineIsActive.
Layout and Fit
The layout system controls how the animation fits and aligns within its container. Use the Fit enum and alignment values to configure this.
Fit Modes
| Fit Value | Description |
Fit.CONTAIN | Scale the animation to fit within the container while maintaining aspect ratio (default). |
Fit.COVER | Scale the animation to cover the entire container while maintaining aspect ratio. |
Fit.FILL | Stretch the animation to fill the container, ignoring aspect ratio. |
Fit.FIT_WIDTH | Scale to match the container's width, maintaining aspect ratio. |
Fit.FIT_HEIGHT | Scale to match the container's height, maintaining aspect ratio. |
Fit.NONE | No scaling applied — renders at original size. |
Setting Layout
You can set the layout using either a numeric alignment pair or the LayoutUtil.Alignment enum:
import com.dotlottie.dlplayer.Fit
// Numeric alignment (x, y) from 0.0 to 1.0
controller.setLayout(Fit.CONTAIN, Pair(0.5f, 0.5f)) // Centered
// Enum-based alignment
controller.setLayout(Fit.COVER, LayoutUtil.Alignment.CENTER)The LayoutUtil.Alignment enum provides 9 standard positions: TOP_LEFT, TOP_CENTER, TOP_RIGHT, CENTER_LEFT, CENTER, CENTER_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT.
Slots
The Slots API allows you to dynamically customize animation properties at runtime, such as colors, text, images, and more.
Setting Individual Slots
val controller = remember { DotLottieController() }
// Set a color slot (ARGB integer)
controller.setColorSlot("primaryColor", Color.Red.toArgb())
// Set a text slot
controller.setTextSlot("headline", "Hello Android!")
// Set a scalar slot
controller.setScalarSlot("opacity", 0.8f)
// Set a vector slot
controller.setVectorSlot("direction", PointF(1.0f, 0.0f))
// Set a position slot
controller.setPositionSlot("anchor", PointF(100f, 200f))
// Set an image slot by file path
controller.setImageSlotPath("avatar", "/path/to/image.png")
// Set an image slot by data URL
controller.setImageSlotDataUrl("logo", "data:image/png;base64,...")Batch Setting Slots
You can set multiple slots at once using a JSON string:
controller.setSlots("""{"primaryColor": [1.0, 0.0, 0.0, 1.0], "headline": {"t": "Hello!"}}""")Clearing Slots
// Clear a specific slot
controller.clearSlot("primaryColor")
// Clear all slots
controller.clearSlots()Multiple Animations .lottie
Initially player renders defaultAnimation from .lottie file. You can change the animation by passing animationId to loadAnimation method on the controller.
Using the manifest().animations you can get the list of animations in the .lottie file.
@Preview()
@Composable
fun DotLottiePreview() {
val controller = remember { DotLottieController() }
val currentIndex = remember { mutableIntStateOf(0) }
Column {
DotLottieAnimation(
source = DotLottieSource.Url("https://lottie.host/294b684d-d6b4-4116-ab35-85ef566d4379/VkGHcqcMUI.lottie"),
autoplay = true,
loop = true,
controller = controller,
)
Button(onClick = {
controller.manifest()?.animations?.let {
currentIndex.intValue =
if (currentIndex.intValue == it.size - 1) 0 else currentIndex.intValue + 1
controller.loadAnimation(it[currentIndex.intValue].id)
}
}) {
Text(text = "Next")
}
Button(onClick = {
controller.manifest()?.animations?.let {
currentIndex.intValue =
if (currentIndex.intValue <= 0) it.size - 1 else currentIndex.intValue - 1
controller.loadAnimation(it[currentIndex.intValue].id)
}
}) {
Text(text = "Previous")
}
}
}Theming
Adding themes to your Lottie animations can significantly enhance your designs. See this step-by-step guide↗ for creating themes.
Using themes in your dotLottie animations
You can apply a theme by its ID either during initialization via the themeId parameter or programmatically using the controller.
1. Apply Theme on Load:
import androidx.compose.foundation.isSystemInDarkTheme
@Preview
@Composable
fun ThemedAnimationOnLoad() {
// Example: Sync with system dark mode
val currentThemeId = if (isSystemInDarkTheme()) "dark" else null // Use null for default
DotLottieAnimation(
source = DotLottieSource.Asset("themed_animation.lottie"),
autoplay = true,
loop = true,
themeId = currentThemeId // Apply theme ID here
)
}2. Control Theme with Controller:
@Preview
@Composable
fun ThemeControllerPreview() {
val controller = remember { DotLottieController() }
var currentTheme by remember { mutableStateOf<String?>("light") }
LaunchedEffect(currentTheme) {
if (currentTheme == null) {
controller.resetTheme()
} else {
controller.setTheme(currentTheme!!) // Apply theme via controller
}
}
Column {
DotLottieAnimation(
source = DotLottieSource.Asset("themed_animation.lottie"),
autoplay = true,
loop = true,
controller = controller
)
Row {
Button(onClick = { currentTheme = "light" }) { Text("Light") }
Button(onClick = { currentTheme = "dark" }) { Text("Dark") }
Button(onClick = { currentTheme = null }) { Text("Reset") }
}
Text("Active Theme: ${controller.activeThemeId.collectAsState().value ?: "Default"}")
}
}3. Load Theme Data Dynamically:
You can also load theme configurations directly from a JSON string.
val themeJson = """{"id":"runtimeTheme","values":{"color":"#ff0000"}} """
// In a coroutine scope or LaunchedEffect:
val success = controller.setThemeData(themeJson)
if (success) {
Log.d("Theming", "Runtime theme applied")
} else {
Log.e("Theming", "Failed to apply runtime theme data")
}State Machines
State machines let you define interactive animation logic — states, transitions, guards, and interactions — inside a .lottie file. The Android player executes that logic at runtime.
Basic Setup
The simplest way to use a state machine is to pass its ID via the stateMachineId parameter. The player loads and starts it automatically once the animation is ready. A DotLottieController is required so you can interact with the state machine at runtime.
import com.lottiefiles.dotlottie.core.widget.DotLottieController
@Composable
fun StateMachineBasic() {
val controller = remember { DotLottieController() }
DotLottieAnimation(
source = DotLottieSource.Asset("interactive.lottie"),
autoplay = true,
stateMachineId = "myStateMachine",
controller = controller,
modifier = Modifier.fillMaxWidth().aspectRatio(1f)
)
}Pointer and click interactions defined in the state machine's interactions array (e.g. Click, PointerDown, PointerEnter) are handled automatically by the composable. You do not need to add gesture handlers for these.
Programmatic Loading
To load or restart a state machine at a specific point in your app's lifecycle, use the controller directly. Call these methods after the animation has loaded.
@Composable
fun StateMachineProgrammatic() {
val controller = remember { DotLottieController() }
LaunchedEffect(Unit) {
// Wait for animation to load, then load and start the state machine
val loaded = controller.stateMachineLoad("myStateMachine")
if (loaded) {
controller.stateMachineStart()
}
}
DotLottieAnimation(
source = DotLottieSource.Asset("interactive.lottie"),
autoplay = true,
controller = controller,
modifier = Modifier.fillMaxWidth().aspectRatio(1f)
)
}You can also load a state machine from a raw JSON string — useful for state machines not bundled inside the .lottie file:
val json = context.assets.open("my_state_machine.json").bufferedReader().readText()
val loaded = controller.stateMachineLoadData(json)
if (loaded) {
controller.stateMachineStart()
}To stop the state machine:
controller.stateMachineStop()Setting Inputs
State machine inputs are named variables (Boolean, Numeric, String) that guard conditions read to decide whether a transition fires. Set them from your app code at any time after the state machine has started.
// Boolean
controller.stateMachineSetBooleanInput("isActive", true)
// Numeric
controller.stateMachineSetNumericInput("progress", 75f)
// String
controller.stateMachineSetStringInput("mode", "dark")Read them back if needed:
val isActive = controller.stateMachineGetBooleanInput("isActive")
val progress = controller.stateMachineGetNumericInput("progress")
val mode = controller.stateMachineGetStringInput("mode")Firing Events
Event-type inputs are edge-triggered: setting one causes a transition to fire immediately if a matching guard exists. Use stateMachineFire to trigger them by name:
controller.stateMachineFire("buttonClicked")A common pattern is wiring a UI element to fire an event:
Button(onClick = { controller.stateMachineFire("next") }) {
Text("Next")
}Listening to State Machine Events
Implement StateMachineEventListener and register it on the controller to observe state changes, input changes, and custom events. All methods have default empty implementations — only override what you need.
val smListener = object : StateMachineEventListener {
override fun onStateEntered(enteringState: String) {
Log.d("SM", "Entered: $enteringState")
}
override fun onStateExit(leavingState: String) {
Log.d("SM", "Exited: $leavingState")
}
override fun onTransition(previousState: String, newState: String) {
Log.d("SM", "$previousState → $newState")
}
override fun onInputFired(inputName: String) {
Log.d("SM", "Event fired: $inputName")
}
override fun onCustomEvent(message: String) {
Log.d("SM", "Custom event: $message")
}
override fun onBooleanInputValueChange(inputName: String, oldValue: Boolean, newValue: Boolean) {
Log.d("SM", "$inputName: $oldValue → $newValue")
}
override fun onNumericInputValueChange(inputName: String, oldValue: Float, newValue: Float) {
Log.d("SM", "$inputName: $oldValue → $newValue")
}
override fun onError(message: String) {
Log.e("SM", "Error: $message")
}
}
LaunchedEffect(Unit) {
controller.stateMachineAddEventListener(smListener)
}Remove listeners when they are no longer needed to avoid leaks:
DisposableEffect(Unit) {
onDispose { controller.stateMachineRemoveEventListener(smListener) }
}URL Opening
If your state machine uses OpenUrl actions, configure how URLs are handled via OpenUrlPolicy when calling stateMachineStart:
controller.stateMachineStart(
openUrl = OpenUrlPolicy(
requireUserInteraction = true,
whitelist = listOf("https://lottiefiles.com/*")
),
onOpenUrl = { url ->
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
context.startActivity(intent)
}
)| Property | Default | Description |
requireUserInteraction | false | When true, the URL opens only as a direct result of a user gesture. |
whitelist | [] | List of allowed URL patterns. An empty list allows all URLs. |
Complete Example
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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 android.util.Log
@Composable
fun InteractiveAnimation() {
val controller = remember { DotLottieController() }
var currentState by remember { mutableStateOf("—") }
val smListener = remember {
object : StateMachineEventListener {
override fun onStateEntered(enteringState: String) {
currentState = enteringState
}
override fun onTransition(previousState: String, newState: String) {
Log.d("SM", "$previousState → $newState")
}
override fun onCustomEvent(message: String) {
Log.d("SM", "Custom event: $message")
}
override fun onError(message: String) {
Log.e("SM", "Error: $message")
}
}
}
LaunchedEffect(Unit) {
controller.stateMachineAddEventListener(smListener)
}
DisposableEffect(Unit) {
onDispose { controller.stateMachineRemoveEventListener(smListener) }
}
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
DotLottieAnimation(
source = DotLottieSource.Asset("interactive.lottie"),
autoplay = true,
stateMachineId = "mainFSM",
controller = controller,
modifier = Modifier.fillMaxWidth().aspectRatio(1f)
)
Text("State: $currentState")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { controller.stateMachineSetBooleanInput("isActive", true) }) {
Text("Activate")
}
Button(onClick = { controller.stateMachineFire("reset") }) {
Text("Reset")
}
}
}
}