optimize performance for modern Android apps.">
Skip to content

Recommended Reading

Jetpack Compose Animations: A Complete Guide for Android Developers

20 Minutes

Jetpack Compose Animations: A Complete Guide for Android Developers

Fix Bugs Faster! Log Collection Made Easy

Get started

Jetpack Compose is Android’s modern UI toolkit. It unifies and simplifies the experience by consolidating state and logic in the rendering with a more declarative approach. Among its most notable features is a powerful animation framework that helps developers create fluid and performant animations with ease. Jetpack Compose allows you to easily inject animations into your UI, which is a great way of providing visual feedback and enhancing interactions that directly affect the user experience.

In this article, we will drill down on the top-level concepts and skills of Jetpack Compose animations. After reading this post, you will have an understanding of how to create complex animations, learn how to work with a variety of animation APIs, and render smoother, more performance-friendly animations. We will cover:

  • Animations that are specific to a state of your UI, so even when the animation ends, they stay balanced.
  • Interactions of the user to gesture-driven animations
  • Transition animations animating multiple properties at once
  • Advanced handling of the appearance and disappearance of composables using AnimatedVisibility.

This article is mainly for experienced Android developers who have worked with the basics of Jetpack Compose and are looking to level up their animation game. It will also be advantageous for developers who previously developed UI using XML and transitioned to Jetpack Compose, and are now looking to make user-friendly apps with dynamic interfaces that looks amazing. Regardless of whether you are aiming for greater visual aesthetics, or if you want to grasp the extent of Compose animations capabilities, this article will be helpful to you.

Understanding the animation APIs in Jetpack Compose

Overview of animation APIs

Jetpack Compose provides multiple animation APIs and each of them is custom designed for its specific need. The following are the main Animation APIs offered in Jetpack Compose:

  • animate*AsState: These functions animate a single state in response to the change; essentially this will give your screenplay an animated pulsing color. These are very easy to use for simple animations, such as animating a property when the state changes. //Example of animateColorAsState val color by animateColorAsState(if (isRed) Color.Red else Color.Blue)
  • Animatable: This API is great when creating more complex animations, or if you need programmatic control over an animation. The Animatable protocol can animate any value and allows a very detailed control over the state of an animation. val offsetX = remember { Animatable(0f) } //Example of Animatable
  • Transition: This API helps to animate multiple properties at once. It offers a method for describing animated changes with various states, therefore allowing the easier coordination of more elaborate animations. //Example of updateTransition val transition = updateTransition(targetState = isExpanded)
  • AnimatedVisibility: Compose provides this API for animating the appearance and disappearance of composables. It includes built-in enter and exit animations for when elements come onto or leave the screen. //Example of AnimatedVisibility AnimatedVisibility(visible = isVisible) { Box(modifier = Modifier.size(100.dp).background(Color.Magenta)) }

How to choose the right API

The choice of animation API is based on your use case.

  1. Simple state changes: When you want to animate a single property as the state changes, use animate*AsState. The API is user-friendly and provides a good solution in the easy-to-manage cases like changing colors, sizes and offsets. //Example of using animateColorAsState val color by animateColorAsState(if (isClicked) Color.Green else Color.Gray)
  2. Programmatic control: Use when you need finer control over an animation – for example, when starting, reversing or updating it in response to user interactions or other app events. //Example of Animatable val offsetX = remember { Animatable(0f) }
  3. Coordinated animations: If you want to animate a bunch of properties at the same time, use Transition. This helps to identify differences when in transit, making sure all changes animate together with a correct property value. //updateTransition example val transition = updateTransition(targetState = isExpanded)
  4. Visibility animations: AnimatedVisibility is used to animate the visible and invisible state of any composable The API takes care of visibility changes and implements entering-to-exiting screen transitions. //AnimatedVisibility example AnimatedVisibility(visible = showDialog) { // Dialog content here }

Simple animations with animate*AsState

These are simple animations created using animate*AsState functions in Jetpack Compose and made specifically for state-driven animation. These functions handle the complexity of animating values when state changes, so you don’t need to manage the updates manually. animateColorAsState, animateDpAsState, animateFloatAsState are used for smooth transitions.

//use animateColorAsState to animate the color of a Box
@Composable
fun ColorAnimationDemo() {
    var isRed by remember { mutableStateOf(true) }
    val color by animateColorAsState(targetValue = if (isRed) Color.Red else Color.Blue)

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(color)
            .clickable { isRed = !isRed }
    )
}

Customizing Animation Parameters

These animate*AsState functions have parameters that allow you to customize the animations – either for duration, or so that the animation fits better with your design needs. animationSpec parameter is used to adjust the parameters.

//example of customizing the animation duration
@Composable
fun CustomColorAnimationDemo() {
    var isRed by remember { mutableStateOf(true) }
    val color by animateColorAsState(
        targetValue = if (isRed) Color.Red else Color.Blue,
        animationSpec = tween(
            durationMillis = 1000, // 1 second duration
            easing = LinearOutSlowInEasing, // Easing curve
            delayMillis = 300 // 300 ms delay
        )
    )

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(color)
            .clickable { isRed = !isRed }
    )
}

Using Animatable for complex animations

The Animatable class is a powerful tool for creating more complex and programmatic animations in Jetpack Compose. While animate*AsState functions are fairly simple state-driven animations, Animatable is more granular, allowing you to control animations. You can play, stop, reverse or chain Animations together along with some more advanced use cases.

Animatable can animate properties like color, size or position.

Using Animatable to change the position:

//Example of Animating Position
@Composable
fun DraggableBox() {
    val offsetX = remember { Animatable(0f) }
    val offsetY = remember { Animatable(0f) }

    Box(
        modifier = Modifier
            .size(100.dp)
            .offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .background(Color.Green)
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    change.consume()
                    offsetX.snapTo(offsetX.value + dragAmount.x)
                    offsetY.snapTo(offsetY.value + dragAmount.y)
                }
            }
    )
}

Using Animatable to change the color:

//Example of Animating Color
@Composable
fun ColorChangeBox() {
    val color = remember { Animatable(Color.Red) }

    LaunchedEffect(key1 = Unit) {
        color.animateTo(Color.Blue, animationSpec = tween(durationMillis = 2000))
    }

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(color.value)
            .clickable {
                LaunchedEffect(key1 = Unit) {
                    color.animateTo(if (color.value == Color.Blue) Color.Red else Color.Blue)
                }
            }
    )
}

Leveraging Transition for state transitions

Jetpack Compose provides Transition API to ease the native handling of complicated state transitions takes with animating multiple property at once. This API allows transitions between states to be defined in a declarative manner, ensuring animations work without any visual hiccups.

How to create a Simple Transition

The first is using the Transition API to create a transition object with updateTransition, within which you can then specify animated properties. Here’s a simple example of how that works:

// example where a Box animates its size and color 
@Composable
fun SimpleTransitionDemo() {
    var isExpanded by remember { mutableStateOf(false) }
    val transition = updateTransition(targetState = isExpanded, label = "BoxTransition")

    val size by transition.animateDp(label = "SizeAnimation") { state ->
        if (state) 200.dp else 100.dp
    }

    val color by transition.animateColor(label = "ColorAnimation") { state ->
        if (state) Color.Red else Color.Blue
    }

    Box(
        modifier = Modifier
            .size(size)
            .background(color)
            .clickable { isExpanded = !isExpanded }
    )
}

Customizing the Transition

The Transition API focuses on animation parameters like duration, easing and delay for all those animated values. The animateDp and animateColor functions can take an AnimationSpec to determine how you want the animation performed.

//Example of Custom Animation Parameters
@Composable
fun CustomTransitionDemo() {
    var isExpanded by remember { mutableStateOf(false) }
    val transition = updateTransition(targetState = isExpanded, label = "BoxTransition")

    val size by transition.animateDp(
        label = "SizeAnimation",
        transitionSpec = { tween(durationMillis = 1000, easing = LinearOutSlowInEasing) }
    ) { state ->
        if (state) 200.dp else 100.dp
    }

    val color by transition.animateColor(
        label = "ColorAnimation",
        transitionSpec = { tween(durationMillis = 1000, easing = FastOutSlowInEasing) }
    ) { state ->
        if (state) Color.Red else Color.Blue
    }

    Box(
        modifier = Modifier
            .size(size)
            .background(color)
            .clickable { isExpanded = !isExpanded }
    )
}

Gesture-Based Animations

Animating Based on Gestures

Animations combined with gestures can help make UI components feel dynamic and highly interactive. Jetpack Compose’s Modifier.pointerInput lets you detect and respond to other user gestures – like dragging, swiping or tapping.

This lets you tie animations directly to user gestures. By combining Animatable with gesture detection, you can create smooth, seamless and gesture-driven animations.

//Example of Box dragged around the screen
@Composable
fun DraggableBox() {
    val offsetX = remember { Animatable(0f) }
    val offsetY = remember { Animatable(0f) }

    Box(
        modifier = Modifier
            .size(100.dp)
            .offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .background(Color.Green)
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    change.consume()
                    launch {
                        offsetX.snapTo(offsetX.value + dragAmount.x)
                        offsetY.snapTo(offsetY.value + dragAmount.y)
                    }
                }
            }
    )
}

Swipe to dismiss example:

//example where a Box can be swiped to the left or right to dismiss it
@Composable
fun SwipeToDismissBox() {
    val offsetX = remember { Animatable(0f) }
    val boxWidth = 100.dp
    val screenWidth = LocalConfiguration.current.screenWidthDp.dp

    Box(
        modifier = Modifier
            .size(boxWidth)
            .offset { IntOffset(offsetX.value.roundToInt(), 0) }
            .background(Color.Red)
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragEnd = {
                        launch {
                            if (offsetX.value.absoluteValue > screenWidth.value / 4) {
                                offsetX.animateTo(if (offsetX.value > 0) screenWidth.value.toFloat() else -screenWidth.value.toFloat())
                            } else {
                                offsetX.animateTo(0f)
                            }
                        }
                    }
                ) { change, dragAmount ->
                    change.consume()
                    launch {
                        offsetX.snapTo(offsetX.value + dragAmount.x)
                    }
                }
            }
    )
}

Performance optimization for animations

Reducing Recomposition

In Jetpack Compose, recomposition is the process of re-executing composable functions when state changes. Although recomposition is an efficient operation in Compose, you must be careful not to rebuild it too frequently as otherwise the performance may suffer.

  1. Use remember to store state that doesn’t need to be – and, indeed, should be not be – recomposed. This ensures you don’t recompose when nothing has changed, and preserves the state over recompositions. val offsetX = remember { Animatable(0f) }
  2. Use rememberUpdatedState inside effects or lambdas to reference the latest value of a state without restarting the effect. This ensures your animations and side effects stay in sync with state changes.
val currentValue by rememberUpdatedState(newValue)
  1. Avoid heavy computations in your composables and instead just pass results of those computations to the composable @Composable fun MyComposable() { val computedValue = remember { performHeavyComputation() } // Use computedValue in the UI }
  2. Launch coroutines in Compose with LaunchedEffect – or for non-suspending side effects use SideEffect. This ensures the side effects will not be executed unnecessarily.
LaunchedEffect(key1 = someKey) {
    // Coroutine logic
}
SideEffect {
    // Non-suspending side effects
}

Real-World use cases

Case study of E-Commerce application with high-level animations

Below is a real example of advanced animations applied to an e-commerce application. This case study focuses on animating the transition between a product list and a product detail page.

Product detail page transition

In an e-commerce app, providing a smooth transition between page and other UI elements can significantly enhance the user experience. We can use a combination of Transition and AnimatedVisibility to achieve this effect.

The result is a seamless transition from the list of products to details of a selected product. For this effect we can use Transition with AnimatedVisibility.

@Composable
fun ProductListItem(product: Product, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable(onClick = onClick)
    ) {
        Row {
            Image(
                painter = rememberImagePainter(product.imageUrl),
                contentDescription = null,
                modifier = Modifier.size(100.dp)
            )
            Text(
                text = product.name,
                modifier = Modifier
                    .padding(16.dp)
                    .weight(1f)
            )
        }
    }
}

@Composable
fun ProductDetailPage(product: Product, onClose: () -> Unit) {
    var isVisible by remember { mutableStateOf(true) }
    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn() + expandIn(),
        exit = fadeOut() + shrinkOut()
    ) {
        Column {
            Button(onClick = { isVisible = false; onClose() }) {
                Text("Back")
            }
            Image(
                painter = rememberImagePainter(product.imageUrl),
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().height(200.dp)
            )
            Text(
                text = product.name,
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(16.dp)
            )
            Text(
                text = product.description,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

Best Practices for Smooth Animations

  • Use remember to store state and avoid unnecessary recompositions. Avoid doing heavy computations in composables.
  • Use the profiling tools in Android Studio to monitor performance of your app and identify ways to optimize.
  • Select the right AnimationSpec for a smooth transition and better performance.
  • Avoid overdraw by minimizing overlapping composables.
  • Use coroutines with Animatable for complex, sequential animations that don’t block the main thread.
  • Efficiently manage rapid gesture events by debouncing them, which reduces the number of animations.

To Sum up

In this article, we learned why animations are important for improving the UX, and examined various animation APIs provided by Jetpack Compose. We also:

  • Learned about animations powered by state using functions like animate*AsState, as well as how to customize parameters like duration, easing and delays.
  • Went in-depth on the Animatable class, supporting richer animations like changing color, size and position via coroutines.
  • Experimented with the Transition API to animate multiple properties which make state transitions complex.
  • Applied AnimatedVisibility to handle the appearance and disappearance of composables.
  • Integrated animations with gestures usingModifier.pointerInput to create responsive, interactive UI elements.
  • Talked about ways to optimize animations for better performance by reducing the recompositions, in addition to profiling our animations with the tools in Android Studio.
  • Walked through a case study of more complicated animations in an e-commerce app and methods to keep things performant smooth. Happy Coding!

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Sachin Siwal

Sachin is an accomplished iOS developer and seasoned technical manager, combining 12 years of mobile app expertise with a knack for leading and delivering innovative solutions. You can contact him on Linkedin

Join thousands of developers
and start fixing bugs faster than ever.