Green Apple Lot by RP Photography
Shimmer is an effect that improves the user experience by providing a smoother transition between the “loading” and “ready” states by showing a skeleton of the UI that is about to be shown. This effect reduces the user uncertainty about what will be shown, and it’s widely used in applications that rely on remote data.
The goal of this article is not to show how to implement the shimmer effect, there are multiple interesting articles about this topic. This article will cover how to seamlessly integrate it with your existent Compose code.
Challenge
Recently, I was tasked to rebuild a screen that loads content from the server and display to the user. During the development, I thought it would be nice to add a shimmer effect since the request took a few seconds and the screen doesn’t have a cache mechanism.
Loading without shimmer
Loading with shimmer
My first attempt was to create a new Composable to “mirror” the real one, having the shimmer effect. The benefit of this approach is that since there is no data ready, we can draw shapes in the same position as the real components. However, maintaining two different sets of Composables is difficult, especially for more complex screens that have multiple nested ones. In addition, all the UI changes in the original Composable needs to be ported to these “mirrored” versions to keep the consistency.
After some investigation, I decided to mix-and-match some solutions and develop a way to use the shimmer, modifying as little as possible the existing Composables.
Implementation
Adding the shimmer effect
As mentioned earlier, there are several references on how to implement your custom shimmer effect. To keep this article simple and focused on the integration, we will use the valentinilk/compose-shimmer library, which is easy to use and have multiplatform support.
Implementing a new Modifier
We will implement a new Modifier to wrap the shimmer effect and draw the placeholder, if needed. Here is the proposed code:
@Composable | |
fun Modifier.shimmerable( | |
enabled: Boolean, | |
color: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), | |
shape: Shape = RoundedCornerShape(8.dp), | |
): Modifier { | |
if (!enabled) return this | |
return this | |
.shimmer() // 3rd party library call | |
.background(color = color, shape = shape) | |
.drawWithContent { | |
// Do not draw the actual content. | |
} | |
} |
Analyzing the code above, the implementation:
- Receives a boolean and have an early return if the effect should not be enabled
- Receives a color and shape that can be customized
- If enabled, the Modifier will draw the shape where the content should be and do not draw the content at all
Here’s how the Compose Preview will look like:
Preview: shimmer disabled (top) vs. shimmer enabled (bottom)
For using it, all we need to do is adding the Modifier to the Composables we want to have this support:
@Composable | |
private fun ItemCard( | |
item: ItemData, | |
isLoading: Boolean = false | |
modifier: Modifier = Modifier | |
) { | |
Text( | |
text = item.description, | |
style = MaterialTheme.typography.bodyMedium, | |
modifier = Modifier.shimmerable(enabled = isLoading) | |
) | |
// [...] Remaining Composable code | |
} |
Integrating with the existing code
As you might have noticed, even though we are sending the isLoading
value to enable the shimmer effect, since the code is loading the data from the server, we don’t have ItemData
available yet to display. To help solve this issue, we can apply the concepts:
- Make the Composable stateless, making it easy to integrate any data without introducing side effects;
- Create a fake representation of the data, similarly to the data we will use in the Composable Preview
After applying the concepts above, we would have a code similar to the following:
// [...] Composable screen code | |
when (state) { | |
MainState.Loading -> { | |
ItemCard( | |
item = fakeData, | |
isLoading = true, | |
) | |
} | |
is MainState.Success -> { | |
ItemCard( | |
item = state.itemData, | |
isLoading = false, | |
) | |
} | |
} | |
// Fake data to be used while loading and in the preview | |
private val fakeData = ItemData( | |
imageResId = android.R.drawable.ic_dialog_info, | |
title = LoremIpsum(4).values.toList().first(), | |
description = LoremIpsum(8).values.toList().first(), | |
) | |
@PreviewLightDark | |
@Composable | |
fun ItemDataPreview() { | |
ExampleTheme { | |
ItemCard(item = fakeData) | |
} | |
} |
Job Offers
Optional: Implement a custom CompositionLocal
Before diving into the implementation, it’s important to notice that CompositionLocal should be used with care, since its over-usage adds extra complexity to the code. For more information about this component and when or not use, please access the official docs.
The solution we developed worked good so far, however the original goal was to change the original Composable code as little as possible. In the current state, we need to pass the isLoading: Boolean
around to determine if theModifier.shimmerable
should be enabled or not.
By implementing a custom CompositionLocal, we can pass this information down the Composable nodes implicitly, making the Composables unaware of this value since it only matters for our custom Modifier. One possible implementation for this feature is:
@Composable | |
fun Modifier.shimmerable( | |
shape: Shape = RoundedCornerShape(8.dp), | |
color: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) | |
): Modifier { | |
if (!LocalShimmerState.current.isLoading) return this | |
return this | |
.shimmer() | |
.background(color = color, shape = shape) | |
.drawWithContent { | |
// Do not draw the actual content. | |
} | |
} | |
data class ShimmerState(val isLoading: Boolean) | |
val LocalShimmerState = compositionLocalOf { ShimmerState(isLoading = false) } | |
@Composable | |
fun ShimmerProvider(isLoading: Boolean = true, content: @Composable (Boolean) -> Unit) { | |
CompositionLocalProvider( | |
value = LocalShimmerState provides ShimmerState(isLoading = isLoading), | |
content = { content(isLoading) }, | |
) | |
} |
The usage will be simplified to:
// [...] Composable screen code | |
when (state) { | |
MainState.Loading -> { | |
ShimmerProvider { | |
ItemCard(item = fakeData) | |
} | |
} | |
is MainState.Success -> { | |
ItemCard(item = state.itemData) | |
} | |
} | |
@Composable | |
private fun ItemCard( | |
item: ItemData, | |
modifier: Modifier = Modifier | |
) { | |
Text( | |
text = item.description, | |
style = MaterialTheme.typography.bodyMedium, | |
modifier = Modifier.shimmerable() | |
) | |
// [...] Remaining Composable code | |
} |
After this changes, we only need two changes in our code:
- Wrap the loading state with the shimmer with our CompositionLocal
- Add
.shimmerable()
in all the Composables we want to have this support
What’s next?
As usual, the code used here is available in full on GitHub. Feel free to use and modify it as you wish. 😊
Conclusion
The shimmer effect is a well known strategy to improve the user experience by creating a smoother transition. Implementing the shimmer effect in Compose is well documented and there are libraries to help us on this task. However, making it easy to implement on existing codebases and keep it scalable might be a challenge. Hopefully, this article help you have some ideas on how to achieve it.
Thank you so much for reading my article! ❤️
This article is previously published on proandroiddev.com.