Compose has built-in, declarative animation APIs:
@Composable
fun ExpandableCard(title: String, body: String) {
var expanded by remember { mutableStateOf(false) }
val rotation by animateFloatAsState(
targetValue = if (expanded) 180f else 0f,
label = "chevron-rotation"
)
Card(modifier = Modifier.clickable { expanded = !expanded }) {
Column(Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(title, modifier = Modifier.weight(1f))
Icon(
Icons.Default.ExpandMore,
contentDescription = null,
modifier = Modifier.rotate(rotation)
)
}
AnimatedVisibility(visible = expanded) {
Text(body, modifier = Modifier.padding(top = 8.dp))
}
}
}
}
Side effects: LaunchedEffect
Composable functions must be free of side effects (like starting a coroutine directly) since they can recompose many times. LaunchedEffect safely runs suspend code tied to a composable’s lifecycle:
@Composable
fun MovieDetailScreen(movieId: Int, viewModel: MovieDetailViewModel = hiltViewModel()) {
LaunchedEffect(movieId) {
viewModel.loadMovie(movieId) // re-runs only if movieId changes
}
// ... render UI from viewModel's state
}
The movieId key parameter matters: LaunchedEffect only re-launches its block when the key changes, not on every recomposition — essential for avoiding redundant network calls.
- Image
- SKU
- Rating
- Price
- Stock
- Availability
- Add to cart
- Description
- Content
- Weight
- Dimensions
- Additional information