Hello! My name is Artem, and I develop Android applications. Recently, I started sharing my experiences on this topic. This article is a text version of a video on my YouTube channel Android Insights. In this article, I want to explain how to animate your app using Shared Element Transition. This feature allows UI elements to animate as they transition between app screens, making the user interface more memorable. This feature was recently added, so ensure your project uses Jetpack Compose version 1.7.0-alpha07 or higher. To demonstrate, I've created a simple app with just two screens: a list on the first screen and detailed information on the second. For simplicity, I'm not using any navigation libraries. Here's the code for the root @Composable function: @Composable fun CatsContent(modifier: Modifier) { var selectedCat: Cat? by remember { mutableStateOf(null) } if (selectedCat != null) { BackHandler { selectedCat = null } CatDetails( modifier = modifier.fillMaxSize(), cat = selectedCat!!, ) } else { CatsList( modifier = modifier.fillMaxSize(), onCatClicked = { cat -> selectedCat = cat } ) } } When an item in the list is clicked, it is assigned to the selectedCat variable. If selectedCat is not null, the @Composable with the detailed information is displayed. Here's the code for CatDetails and CatsList functions: @Composable fun CatDetails( modifier: Modifier, cat: Cat, ) { Column( modifier = modifier.verticalScroll( state = rememberScrollState(), ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Image( painter = painterResource(cat.iconRes), contentDescription = null, ) Text( modifier = Modifier .padding( horizontal = 8.dp, ), text = stringResource(cat.textRes) ) } } @Composable fun CatsList( modifier: Modifier, onCatClicked: (Cat) -> Unit, ) { val cats = rememberCatsList() Box( modifier = modifier, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { items( items = cats, key = { cat -> cat.iconRes }, contentType = { cat -> cat::class } ) { item -> Cat( modifier = Modifier .fillParentMaxWidth() .clickable { onCatClicked(item) } .padding( horizontal = 8.dp ), cat = item, ) } } } } @Composable fun Cat( modifier: Modifier, cat: Cat, ) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( modifier = Modifier.size(128.dp), painter = painterResource(cat.iconRes), contentScale = ContentScale.Crop, contentDescription = null, ) Text( modifier = Modifier, text = stringResource(cat.textRes), maxLines = 3, overflow = TextOverflow.Ellipsis, ) } } Adding Transition Now, let's add the rest of the required code in two steps // OptIn is required because ExperimentalSharedTransitionApi is unstable @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatsContent( modifier: Modifier ) { var selectedCat: Cat? by remember { mutableStateOf(null) } SharedTransitionLayout { AnimatedContent( targetState = selectedCat != null ) { targetState -> if (targetState) { BackHandler { selectedCat = null } CatDetails( modifier = modifier.fillMaxSize(), cat = selectedCat!!, ) } else { CatsList( modifier = modifier.fillMaxSize(), onCatClicked = { cat -> selectedCat = cat } ) } } } } Application behavior has already changed There are animations for transitioning between screens using the AnimatedContent function, but this is still not what we need. It's time to add the rest of the code. @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatsList( modifier: Modifier, onCatClicked: (Cat) -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { val cats = rememberCatsList() Box( modifier = modifier, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { items( items = cats, key = { cat -> cat.iconRes }, contentType = { cat -> cat::class } ) { item -> Cat( modifier = Modifier .fillParentMaxWidth() .clickable { onCatClicked(item) } .padding( horizontal = 8.dp ), cat = item, // passing SharedTransitionScope sharedTransitionScope = sharedTransitionScope, // passing AnimatedVisibilityScope animatedVisibilityScope = animatedVisibilityScope, ) } } } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Cat( modifier: Modifier, cat: Cat, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { with(sharedTransitionScope) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( modifier = Modifier .size(128.dp) // using brand new Modifier sharedElement .sharedElement( state = rememberSharedContentState(key = cat.iconRes.toString()), animatedVisibilityScope = animatedVisibilityScope, ), painter = painterResource(cat.iconRes), contentScale = ContentScale.Crop, contentDescription = null, ) Text( modifier = Modifier, text = stringResource(cat.textRes), maxLines = 3, overflow = TextOverflow.Ellipsis, ) } } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatDetails( modifier: Modifier, cat: Cat, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { with(sharedTransitionScope) { Column( modifier = modifier.verticalScroll( state = rememberScrollState(), ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Image( // using brand new Modifier sharedElement modifier = Modifier.sharedElement( state = rememberSharedContentState(key = cat.iconRes.toString()), animatedVisibilityScope = animatedVisibilityScope, ), painter = painterResource(cat.iconRes), contentDescription = null, ) Text( modifier = Modifier .padding( horizontal = 8.dp, ), text = stringResource(cat.textRes) ) } } } This time, everything looks as it was intended. The Image moves animatedly from one screen to another What changed in the code? The use of a Modifier has been added: Modifier.sharedElement This Modifier, along with the SharedTransitionLayout, is responsible for the magic of moving shared content. In order to be able to use it, you must be inside the SharedTransitionScope. This can be achieved in several ways: // use the function with/run/apply etc. with(sharedTransitionScope) // create an extension to SharedTransitionScope fun SharedTransitionScope.Cat // use context receivers, in which case you can move the // AnimatedVisibilityScope there so as not to pass it as a parameter context(SharedTransitionScope, AnimatedVisibilityScope) @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Cat This is the minimum set of parameters that need to be passed to sharedElement for it to work correctly sharedElement( state = rememberSharedContentState( key = cat.iconRes.toString(), ), animatedVisibilityScope = animatedVisibilityScope, ) state - to track the state of the animation. If there are several elements on the screen that can be animated, then it is imperative to use a unique key for each of the @Composable. animatedVisibilityScope - scope for the transition animation itself You also need to use Modifier.sharedElement on the screen you are navigating to. The key on the target screen must match where the transition started, otherwise nothing will work. Also, since Shared Element Transition has just appeared in Jetpack Compose, everything is in an experimental state and an annotation needs to be added @OptIn(ExperimentalSharedTransitionApi::class) For each function that uses this functionality. Basically, that's all you need to know to get started using Shared Element Transition. Let's take a quick recap Conclusion For using Shared Element Transition in your project: Ensure your Jetpack Compose version is 1.7.0-alpha08 or higher. Wrap your @Composable functions in SharedTransitionLayout. Ensure the keys are passed in rememberSharedContentState are unique for each @Composable to animate and match the keys in the destination. Thanks to all. I hope you found this information helpful! Hello! My name is Artem, and I develop Android applications. Recently, I started sharing my experiences on this topic. This article is a text version of a video on my YouTube channel Android Insights . video Android Insights In this article, I want to explain how to animate your app using Shared Element Transition . This feature allows UI elements to animate as they transition between app screens, making the user interface more memorable. Shared Element Transition This feature was recently added, so ensure your project uses Jetpack Compose version 1.7.0-alpha07 or higher. Jetpack Compose 1.7.0-alpha07 To demonstrate, I've created a simple app with just two screens: a list on the first screen and detailed information on the second. For simplicity, I'm not using any navigation libraries. Here's the code for the root @Composable function: @Composable @Composable fun CatsContent(modifier: Modifier) { var selectedCat: Cat? by remember { mutableStateOf(null) } if (selectedCat != null) { BackHandler { selectedCat = null } CatDetails( modifier = modifier.fillMaxSize(), cat = selectedCat!!, ) } else { CatsList( modifier = modifier.fillMaxSize(), onCatClicked = { cat -> selectedCat = cat } ) } } @Composable fun CatsContent(modifier: Modifier) { var selectedCat: Cat? by remember { mutableStateOf(null) } if (selectedCat != null) { BackHandler { selectedCat = null } CatDetails( modifier = modifier.fillMaxSize(), cat = selectedCat!!, ) } else { CatsList( modifier = modifier.fillMaxSize(), onCatClicked = { cat -> selectedCat = cat } ) } } When an item in the list is clicked, it is assigned to the selectedCat variable. If selectedCat is not null, the @Composable with the detailed information is displayed. selectedCat selectedCat @Composable Here's the code for CatDetails and CatsList functions: CatDetails CatsList @Composable fun CatDetails( modifier: Modifier, cat: Cat, ) { Column( modifier = modifier.verticalScroll( state = rememberScrollState(), ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Image( painter = painterResource(cat.iconRes), contentDescription = null, ) Text( modifier = Modifier .padding( horizontal = 8.dp, ), text = stringResource(cat.textRes) ) } } @Composable fun CatsList( modifier: Modifier, onCatClicked: (Cat) -> Unit, ) { val cats = rememberCatsList() Box( modifier = modifier, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { items( items = cats, key = { cat -> cat.iconRes }, contentType = { cat -> cat::class } ) { item -> Cat( modifier = Modifier .fillParentMaxWidth() .clickable { onCatClicked(item) } .padding( horizontal = 8.dp ), cat = item, ) } } } } @Composable fun Cat( modifier: Modifier, cat: Cat, ) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( modifier = Modifier.size(128.dp), painter = painterResource(cat.iconRes), contentScale = ContentScale.Crop, contentDescription = null, ) Text( modifier = Modifier, text = stringResource(cat.textRes), maxLines = 3, overflow = TextOverflow.Ellipsis, ) } } @Composable fun CatDetails( modifier: Modifier, cat: Cat, ) { Column( modifier = modifier.verticalScroll( state = rememberScrollState(), ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Image( painter = painterResource(cat.iconRes), contentDescription = null, ) Text( modifier = Modifier .padding( horizontal = 8.dp, ), text = stringResource(cat.textRes) ) } } @Composable fun CatsList( modifier: Modifier, onCatClicked: (Cat) -> Unit, ) { val cats = rememberCatsList() Box( modifier = modifier, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { items( items = cats, key = { cat -> cat.iconRes }, contentType = { cat -> cat::class } ) { item -> Cat( modifier = Modifier .fillParentMaxWidth() .clickable { onCatClicked(item) } .padding( horizontal = 8.dp ), cat = item, ) } } } } @Composable fun Cat( modifier: Modifier, cat: Cat, ) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( modifier = Modifier.size(128.dp), painter = painterResource(cat.iconRes), contentScale = ContentScale.Crop, contentDescription = null, ) Text( modifier = Modifier, text = stringResource(cat.textRes), maxLines = 3, overflow = TextOverflow.Ellipsis, ) } } Adding Transition Now, let's add the rest of the required code in two steps // OptIn is required because ExperimentalSharedTransitionApi is unstable @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatsContent( modifier: Modifier ) { var selectedCat: Cat? by remember { mutableStateOf(null) } SharedTransitionLayout { AnimatedContent( targetState = selectedCat != null ) { targetState -> if (targetState) { BackHandler { selectedCat = null } CatDetails( modifier = modifier.fillMaxSize(), cat = selectedCat!!, ) } else { CatsList( modifier = modifier.fillMaxSize(), onCatClicked = { cat -> selectedCat = cat } ) } } } } // OptIn is required because ExperimentalSharedTransitionApi is unstable @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatsContent( modifier: Modifier ) { var selectedCat: Cat? by remember { mutableStateOf(null) } SharedTransitionLayout { AnimatedContent( targetState = selectedCat != null ) { targetState -> if (targetState) { BackHandler { selectedCat = null } CatDetails( modifier = modifier.fillMaxSize(), cat = selectedCat!!, ) } else { CatsList( modifier = modifier.fillMaxSize(), onCatClicked = { cat -> selectedCat = cat } ) } } } } Application behavior has already changed There are animations for transitioning between screens using the AnimatedContent function, but this is still not what we need. AnimatedContent It's time to add the rest of the code. @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatsList( modifier: Modifier, onCatClicked: (Cat) -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { val cats = rememberCatsList() Box( modifier = modifier, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { items( items = cats, key = { cat -> cat.iconRes }, contentType = { cat -> cat::class } ) { item -> Cat( modifier = Modifier .fillParentMaxWidth() .clickable { onCatClicked(item) } .padding( horizontal = 8.dp ), cat = item, // passing SharedTransitionScope sharedTransitionScope = sharedTransitionScope, // passing AnimatedVisibilityScope animatedVisibilityScope = animatedVisibilityScope, ) } } } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Cat( modifier: Modifier, cat: Cat, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { with(sharedTransitionScope) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( modifier = Modifier .size(128.dp) // using brand new Modifier sharedElement .sharedElement( state = rememberSharedContentState(key = cat.iconRes.toString()), animatedVisibilityScope = animatedVisibilityScope, ), painter = painterResource(cat.iconRes), contentScale = ContentScale.Crop, contentDescription = null, ) Text( modifier = Modifier, text = stringResource(cat.textRes), maxLines = 3, overflow = TextOverflow.Ellipsis, ) } } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatDetails( modifier: Modifier, cat: Cat, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { with(sharedTransitionScope) { Column( modifier = modifier.verticalScroll( state = rememberScrollState(), ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Image( // using brand new Modifier sharedElement modifier = Modifier.sharedElement( state = rememberSharedContentState(key = cat.iconRes.toString()), animatedVisibilityScope = animatedVisibilityScope, ), painter = painterResource(cat.iconRes), contentDescription = null, ) Text( modifier = Modifier .padding( horizontal = 8.dp, ), text = stringResource(cat.textRes) ) } } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatsList( modifier: Modifier, onCatClicked: (Cat) -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { val cats = rememberCatsList() Box( modifier = modifier, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { items( items = cats, key = { cat -> cat.iconRes }, contentType = { cat -> cat::class } ) { item -> Cat( modifier = Modifier .fillParentMaxWidth() .clickable { onCatClicked(item) } .padding( horizontal = 8.dp ), cat = item, // passing SharedTransitionScope sharedTransitionScope = sharedTransitionScope, // passing AnimatedVisibilityScope animatedVisibilityScope = animatedVisibilityScope, ) } } } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Cat( modifier: Modifier, cat: Cat, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { with(sharedTransitionScope) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( modifier = Modifier .size(128.dp) // using brand new Modifier sharedElement .sharedElement( state = rememberSharedContentState(key = cat.iconRes.toString()), animatedVisibilityScope = animatedVisibilityScope, ), painter = painterResource(cat.iconRes), contentScale = ContentScale.Crop, contentDescription = null, ) Text( modifier = Modifier, text = stringResource(cat.textRes), maxLines = 3, overflow = TextOverflow.Ellipsis, ) } } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CatDetails( modifier: Modifier, cat: Cat, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { with(sharedTransitionScope) { Column( modifier = modifier.verticalScroll( state = rememberScrollState(), ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Image( // using brand new Modifier sharedElement modifier = Modifier.sharedElement( state = rememberSharedContentState(key = cat.iconRes.toString()), animatedVisibilityScope = animatedVisibilityScope, ), painter = painterResource(cat.iconRes), contentDescription = null, ) Text( modifier = Modifier .padding( horizontal = 8.dp, ), text = stringResource(cat.textRes) ) } } } This time, everything looks as it was intended. The Image moves animatedly from one screen to another What changed in the code? The use of a Modifier has been added: Modifier Modifier.sharedElement Modifier.sharedElement This Modifier, along with the SharedTransitionLayout , is responsible for the magic of moving shared content. SharedTransitionLayout In order to be able to use it, you must be inside the SharedTransitionScope . This can be achieved in several ways: SharedTransitionScope // use the function with/run/apply etc. with(sharedTransitionScope) // create an extension to SharedTransitionScope fun SharedTransitionScope.Cat // use context receivers, in which case you can move the // AnimatedVisibilityScope there so as not to pass it as a parameter context(SharedTransitionScope, AnimatedVisibilityScope) @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Cat // use the function with/run/apply etc. with(sharedTransitionScope) // create an extension to SharedTransitionScope fun SharedTransitionScope.Cat // use context receivers, in which case you can move the // AnimatedVisibilityScope there so as not to pass it as a parameter context(SharedTransitionScope, AnimatedVisibilityScope) @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Cat This is the minimum set of parameters that need to be passed to sharedElement for it to work correctly sharedElement sharedElement( state = rememberSharedContentState( key = cat.iconRes.toString(), ), animatedVisibilityScope = animatedVisibilityScope, ) sharedElement( state = rememberSharedContentState( key = cat.iconRes.toString(), ), animatedVisibilityScope = animatedVisibilityScope, ) state - to track the state of the animation. If there are several elements on the screen that can be animated, then it is imperative to use a unique key for each of the @Composable . state @Composable animatedVisibilityScope - scope for the transition animation itself animatedVisibilityScope You also need to use Modifier.sharedElement on the screen you are navigating to. The key on the target screen must match where the transition started, otherwise nothing will work. Modifier.sharedElement Also, since Shared Element Transition has just appeared in Jetpack Compose , everything is in an experimental state and an annotation needs to be added Shared Element Transition Jetpack Compose @OptIn(ExperimentalSharedTransitionApi::class) @OptIn(ExperimentalSharedTransitionApi::class) For each function that uses this functionality. Basically, that's all you need to know to get started using Shared Element Transition . Let's take a quick recap Shared Element Transition Conclusion For using Shared Element Transition in your project: Shared Element Transition Ensure your Jetpack Compose version is 1.7.0-alpha08 or higher. Wrap your @Composable functions in SharedTransitionLayout. Ensure the keys are passed in rememberSharedContentState are unique for each @Composable to animate and match the keys in the destination. Ensure your Jetpack Compose version is 1.7.0-alpha08 or higher. Ensure your Jetpack Compose version is 1.7.0-alpha08 or higher. Jetpack Compose 1.7.0-alpha08 Wrap your @Composable functions in SharedTransitionLayout. Wrap your @Composable functions in SharedTransitionLayout . @Composable SharedTransitionLayout Ensure the keys are passed in rememberSharedContentState are unique for each @Composable to animate and match the keys in the destination. Ensure the keys are passed in rememberSharedContentState are unique for each @Composable to animate and match the keys in the destination. rememberSharedContentState @Composable Thanks to all. I hope you found this information helpful!