DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Shimmer Loading Effect: Skeleton Screen in Jetpack Compose

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)
}
Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)