Android Jetpack Compose Deep Dive: State, Animation, and Performance
Jetpack Compose has become the standard for Android UI development. With Compose BOM 2026.x, stability is excellent and the API surface is rich. This guide explores advanced patterns for state management, animations, performance optimization, and View system integration.
Understanding Composition and State
State Hoisting Pattern
// Stateless component - easy to test and reuse
@Composable
fun EmailInput(
value: String,
onValueChange: (String) -> Unit,
isError: Boolean = false,
modifier: Modifier = Modifier,
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
label = { Text("Email") },
isError = isError,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = modifier,
)
}
ViewModel with StateFlow
@HiltViewModel
class LoginViewModel @Inject constructor(
private val authRepository: AuthRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun login() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
authRepository.login(_uiState.value.email, _uiState.value.password)
.onSuccess { _uiState.update { it.copy(isLoading = false, loginSuccess = true) } }
.onFailure { e -> _uiState.update { it.copy(isLoading = false, errorMessage = e.message) } }
}
}
}
data class LoginUiState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val loginSuccess: Boolean = false,
val errorMessage: String? = null,
)
Advanced Compose Animations
AnimatedVisibility with Custom Transitions
@Composable
fun NotificationBanner(message: String?, onDismiss: () -> Unit) {
AnimatedVisibility(
visible = message != null,
enter = slideInVertically(
initialOffsetY = { -it },
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow,
)
) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(),
) {
Surface(
color = MaterialTheme.colorScheme.inverseSurface,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth().padding(16.dp),
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(message ?: "", modifier = Modifier.weight(1f))
IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, "Dismiss")
}
}
}
}
}
Animate as State
@Composable
fun ProgressButton(progress: Float, onClick: () -> Unit, modifier: Modifier = Modifier) {
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing),
label = "progress",
)
val backgroundColor by animateColorAsState(
targetValue = if (progress >= 1f)
MaterialTheme.colorScheme.tertiary
else
MaterialTheme.colorScheme.primary,
label = "background",
)
Box(
modifier = modifier
.clip(RoundedCornerShape(50))
.background(backgroundColor)
.clickable(onClick = onClick)
.padding(horizontal = 24.dp, vertical = 12.dp),
) {
LinearProgressIndicator(
progress = { animatedProgress },
modifier = Modifier.fillMaxWidth().height(4.dp).align(Alignment.BottomCenter),
)
Text(
text = if (progress >= 1f) "Done!" else "${(progress * 100).toInt()}%",
color = Color.White,
fontWeight = FontWeight.SemiBold,
)
}
}
Performance Optimization
Avoiding Unnecessary Recompositions
// Use rememberUpdatedState for callbacks
@Composable
fun OptimalExample(count: Int, onCountChanged: (Int) -> Unit) {
val currentOnCountChanged by rememberUpdatedState(onCountChanged)
Button(onClick = { currentOnCountChanged(count + 1) }) {
Text("Count: $count")
}
}
derivedStateOf for Expensive Computations
@Composable
fun SortedList(items: List<Item>) {
val listState = rememberLazyListState()
val sortedItems by remember(items) {
derivedStateOf { items.sortedBy { it.priority } }
}
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 3 }
}
Box {
LazyColumn(state = listState) {
items(sortedItems, key = { it.id }) { item ->
ItemRow(item = item)
}
}
AnimatedVisibility(
visible = showScrollToTop,
modifier = Modifier.align(Alignment.BottomEnd),
) {
FloatingActionButton(
onClick = { /* scroll to top */ },
modifier = Modifier.padding(16.dp),
) {
Icon(Icons.Default.KeyboardArrowUp, "Scroll to top")
}
}
}
}
Stable and Immutable Annotations
@Immutable
data class UserProfile(
val id: String,
val name: String,
val avatarUrl: String,
val bio: String,
)
@Stable
class CartState {
var items by mutableStateListOf<CartItem>()
val total: Double get() = items.sumOf { it.price * it.quantity }
}
Compose and View Interoperability
Embedding Compose in Fragment
class MyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
val viewModel: MyViewModel by viewModels()
MyComposableScreen(viewModel = viewModel)
}
}
}
}
}
Embedding Android View in Compose
@Composable
fun LegacyMapView(latLng: LatLng, modifier: Modifier = Modifier) {
val mapView = rememberMapViewWithLifecycle()
AndroidView(
factory = { mapView },
modifier = modifier,
update = { view ->
view.getMapAsync { map ->
map.moveCamera(CameraUpdateFactory.newLatLng(latLng))
}
},
)
}
Lazy Layout Optimizations
@Composable
fun OptimizedFeed(items: List<FeedItem>, onItemClick: (FeedItem) -> Unit) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
items(
items = items,
key = { item -> item.id },
contentType = { item -> item::class.simpleName },
) { item ->
FeedItemCard(
item = item,
onClick = { onItemClick(item) },
modifier = Modifier.animateItem(),
)
}
}
}
Compose Multiplatform
Compose Multiplatform enables sharing UI code across Android, iOS, desktop, and web:
// Shared UI code
@Composable
fun SharedProfileCard(user: User, modifier: Modifier = Modifier) {
Card(modifier = modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(16.dp)) {
AsyncImage(
model = user.avatarUrl,
contentDescription = null,
modifier = Modifier.size(48.dp).clip(CircleShape),
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(user.name, style = MaterialTheme.typography.titleMedium)
Text(user.bio, style = MaterialTheme.typography.bodySmall)
}
}
}
}
Conclusion
Jetpack Compose continues to mature with each release. Mastering state hoisting, ViewModel integration, animation APIs, and performance optimization enables you to build beautiful, responsive Android apps. The interop story with legacy Views means you can incrementally adopt Compose in existing projects, while new projects benefit from the best developer experience in Android history.