Shimmer Loading Effect: Skeleton Screen in Jetpack Compose
Skeleton screens with shimmer effects improve perceived performance by showing a placeholder while content loads. This guide demonstrates implementing shimmer animations in Jetpack Compose.
Creating a Shimmer Modifier
Build a reusable shimmerEffect() modifier using infiniteRepeatable with linearGradient:
fun Modifier.shimmerEffect(): Modifier = composed {
val shimmerColors = listOf(
Color.LightGray.copy(alpha = 0.6f),
Color.LightGray.copy(alpha = 0.2f),
Color.LightGray.copy(alpha = 0.6f)
)
val transition = rememberInfiniteTransition(label = "shimmer")
val translateAnimation = transition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec = infiniteRepeatable(
animation = tween(800, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "shimmer_animation"
)
val brush = Brush.linearGradient(
colors = shimmerColors,
start = Offset(translateAnimation.value - 200f, 0f),
end = Offset(translateAnimation.value, 0f)
)
background(brush)
}
Skeleton Card Component
Create a reusable skeleton card layout:
@Composable
fun SkeletonCard(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
) {
// Skeleton header image
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.shimmerEffect()
.clip(RoundedCornerShape(8.dp))
)
Spacer(modifier = Modifier.height(12.dp))
// Skeleton title
Box(
modifier = Modifier
.fillMaxWidth(0.7f)
.height(16.dp)
.shimmerEffect()
.clip(RoundedCornerShape(4.dp))
)
Spacer(modifier = Modifier.height(8.dp))
// Skeleton description lines
repeat(2) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(12.dp)
.shimmerEffect()
.clip(RoundedCornerShape(4.dp))
)
Spacer(modifier = Modifier.height(6.dp))
}
}
}
Skeleton List
Display multiple skeleton items for list loading:
@Composable
fun SkeletonListScreen() {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(5) {
SkeletonCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
)
}
}
}
UiState Integration
Manage loading state with sealed classes:
sealed class UiState<T> {
class Loading<T> : UiState<T>()
data class Success<T>(val data: T) : UiState<T>()
data class Error<T>(val exception: Throwable) : UiState<T>()
}
@Composable
fun DataScreen(uiState: UiState<List<Item>>) {
when (uiState) {
is UiState.Loading -> {
SkeletonListScreen()
}
is UiState.Success -> {
ItemList(items = uiState.data)
}
is UiState.Error -> {
ErrorMessage(exception = uiState.exception)
}
}
}
Crossfade Transition
Smoothly transition from skeleton to content:
@Composable
fun LoadingContent(
isLoading: Boolean,
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Crossfade(
targetState = isLoading,
modifier = modifier,
label = "loading_crossfade"
) { loading ->
if (loading) {
SkeletonCard()
} else {
content()
}
}
}
Best Practices
- Match skeleton shape to actual content dimensions
- Use subtle colors for skeletons to minimize jarring transitions
- Animate smoothly with consistent timing (800ms is common)
- Combine with real data fetching for better UX
- Consider accessibility: skeletons should be recognizable
8 Android App Templates → https://myougatheax.gumroad.com
Top comments (0)