“Jetpack Compose makes animations not only possible, but delightful — using state-driven, declarative APIs that feel natural and clean.”
Understanding animate*AsState
in Jetpack Compose
Before we dive into specific animation examples, it’s worth getting familiar with the powerful and beginner-friendly animate*AsState APIs — the foundation of most state-based animations in Jetpack Compose.
What is animate*AsState
?
Jetpack Compose’s animate*AsState
functions help you animate values when state changes. Instead of instantly jumping from one value to another, Compose animates the change smoothly.
There are several built-in versions:
animateDpAsState()
– for sizes and dimensionsanimateFloatAsState()
– for rotation, alpha, progress, etc.animateColorAsState()
– for animating color changesanimateIntAsState()
,animateOffsetAsState()
, etc.
They all share the same structure.
Why it’s great:
- Simple and declarative: Just swap state, and the animation happens.
- Reusable: Works inside any layout or modifier (like
size()
,rotate()
, orpadding()
). - Powerful: Supports different animation specs (
tween
,spring
,keyframes
, etc.)
Now that we understand the magic behind animate*AsState
, let’s explore how to use it in real-world UI animations like size transitions, visibility toggles, and interactive rotations. 👇
In this article, we’ll explore how to create smooth size transitions using one of the simplest but most powerful tools in Jetpack Compose: animateDpAsState,animateFloatAsState and
AnimatedVisibility.
We’ll walk through the example line by line, explain the key concepts like remember
, MutableState
, and tween
, and show how Compose handles animation with minimal effort.
1. Growing and Shrinking a Box
@Composable fun SizeAnimationExample() { var isGrow by remember { mutableStateOf(false) } val boxSize by animateDpAsState( targetValue = if (isGrow) 200.dp else 100.dp, animationSpec = tween(durationMillis = 1000), label = "" ) Column( modifier = Modifier .fillMaxSize() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = { isGrow = !isGrow }) { Text(if (isGrow) "Shrink Box" else "Grow Box") } Spacer(modifier = Modifier.height(16.dp)) Box( modifier = Modifier .size(boxSize) .background(Color.Red) ) } }
Think of it like a switch
This animation works like a light switch:
- When
isGrow
is false, the box is small (100.dp). - When you click the button and flip
isGrow
to true, the box smoothly grows to 200.dp.
So what’s actually happening under the hood? Let’s break it down line by line
Line 1: var isGrow by remember { mutableStateOf(false) }
var isGrow
: This is a mutable Boolean state.remember
: Ensures that the value is retained across recompositions.mutableStateOf(false)
: Creates observable state initialized asfalse
.
When
isGrow changes, Compose automatically triggers a recomposition — redrawing any Composables that depend on it.
Line 2: val boxSize by animateDpAsState(...)
This line is where the animation happens.
animateDpAsState
: Animates aDp
value over time when its target changes.targetValue
: IfisGrow
is true → animate to200.dp
, otherwise →100.dp
.tween(durationMillis = 1000)
: Defines a 1-second smooth animation.boxSize
becomes a smoothly animated value between those two endpoints.
As
isGrow changes,
boxSize changes gradually — and because it’s used inside
Modifier.size(), the box resizes smoothly.
How the UI responds
Let’s walk through the flow:
- On launch,
isGrow = false
→ box starts at100.dp
. - You click the button →
isGrow = true
. animateDpAsState
animates from100.dp
to200.dp
.- The
Box
modifier updates with the new animated size in real time. - Click again → animation reverses.
The result is a polished, native-feeling transition — with no manual animation code, handlers, or frame calculations.
Fading Elements In and Out with AnimatedVisibility
Jetpack Compose offers a very expressive way to show or hide content smoothly using the AnimatedVisibility
composable.
What is AnimatedVisibility
?
Think of it like a magic container:
When a Boolean value (e.g., isVisible
) changes, it handles both the appearance and disappearance of its content with animation.
2. Fade a Box In and Out
@Composable fun VisibilityAnimationTest() { var isVisible by remember { mutableStateOf(true) } Column( modifier = Modifier .fillMaxSize() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = { isVisible = !isVisible }) { Text(if (isVisible) "Hide" else "Show") } Spacer(modifier = Modifier.height(16.dp)) AnimatedVisibility( visible = isVisible, enter = fadeIn(animationSpec = tween(durationMillis = 1000)), exit = fadeOut(animationSpec = tween(durationMillis = 1000)) ) { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) ) } } }

How It Works
1. visible = isVisible
- This controls whether the content should be visible.
isVisible
is aMutableState
value that determines the visibility.- When this value changes, the UI recomposes and the proper animation is triggered.
2. enter = fadeIn(...)
- Defines how the content should appear.
fadeIn()
is a built-in transition that starts at full transparency and fades to visible.tween(durationMillis = 1000)
makes the fade-in last for 1 second.
When isVisible becomes
true, the content fades in.
3. exit = fadeOut(...)
- Defines how the content should disappear.
fadeOut()
gradually fades the content out until it’s fully transparent.- It also uses
tween(durationMillis = 1000)
to last 1 second.
When isVisible becomes
false, the content fades out.
TL;DR — In Simple Terms
AnimatedVisibility
= “Show/Hide with animation”isVisible
= “The switch that controls visibility”fadeIn()
= “Smooth entry”, andfadeOut()
= “Smooth exit”tween(1000)
= “Take 1 second to animate”
Full Lifecycle
- Initial state:
isVisible = true
→ content is shown. - You click the button →
isVisible = false
. AnimatedVisibility
detects the change:- Triggers the exit animation (
fadeOut
) - Fades the content out over 1 second
- Click again →
isVisible = true
: - Triggers enter animation (
fadeIn
) - Content fades back in
All of this happens automatically — no manual animation logic required.
Pro Tip
You’re not limited to just fading. You can combine or replace transitions with:
slideInVertically()
,slideOutHorizontally()
expandVertically()
,shrinkOut()
scaleIn()
,scaleOut()
3. Animating Rotation with animateFloatAsState
Sometimes you want to add a little motion to your interactions like rolling a dice or refreshing content. Here’s how you can do that with animateFloatAsState
:
Setup
var isRolling by remember { mutableStateOf(false) } val rotationAngle by animateFloatAsState( targetValue = if (isRolling) 360f else 0f, animationSpec = tween(durationMillis = 500), finishedListener = { isRolling = false } )
Usage in a Button
IconButton( onClick = { if (!isRolling) { isRolling = true viewModel.fetchZenQuote() } }, modifier = Modifier.rotate(rotationAngle), ) { Icon( imageVector = Icons.Default.Casino, contentDescription = "New quote", tint = Color.White, ) }

Job Offers
How It Works
isRolling
starts asfalse
.- When the user taps the button, it flips
isRolling
totrue
. animateFloatAsState
detects the change and animatesrotationAngle
from0f
→360f
.- Once the spin completes,
finishedListener
resetsisRolling
tofalse
.
This creates a clean, polished 360° spin effect every time the button is pressed. It’s perfect for dice rolls, card shuffles, or random content refreshes.
Why This Works So Well in Compose
Jetpack Compose animations are:
- Declarative — Just describe the end state.
- State-driven — Powered by
remember
andMutableState
. - Composable — Plug into any layout or modifier easily.
You don’t manage time or frames and Compose handles that internally. All you do is update state, and the animation system figures out how to transition the UI.
Follow me for more Android and Tech content!
Let’s connect on Linkedin!
This article was previously published on proandroiddev.com.