Compose Previews & Preview Parameters
Jetpack Compose previews are a powerful tool for rendering your app's UI in the IDE in realtime¹.
Compose Previews
Say you have the following Composable
:
@Composable fun CustomText(text: String) {
Text(text = text)
}
You can preview this composable, like so:
@Preview
@Composable
fun CustomTextPreview() {
CustomText(text = "Wow, a preview!")
}
Which will render a preview of your composable, in the IDE 'design' tab:
Ew. Let's see what this might look like on an actual screen, by adding a Scaffold
around our preview content:
@Preview
@Composable
fun CustomTextPreview() {
Scaffold {
CustomText(text = "Wow, a preview!")
}
}
That's right, you can wrap your Preview content in other Composables!
Preview Annotations
What if we want to see this preview in dark mode? Compose Previews accept annotation arguments such as device
, showBackground
, uiMode
, name
, group
, apiLevel
, showSystemUI
, fontScale
and a bunch of others. There's a UI in the IDE for setting these.
Font Scale
I find fontScale
to be a particularly useful parameter, for quickly previewing whether text content is cut off.
Dark mode
Let's try setting showBackground=true
and uiMode=night
:
@Preview(
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun CustomTextPreview() {
Scaffold() {
CustomText(text = "Wow, a preview!")
}
}
Wait a minute, that looks the same. Where's my dark mode?
Well, we haven't actually instructed the CustomText
composable, nor the CustomTextPreview
to be aware of the theming system. Let's render the preview content inside a theme-aware Composable:
@Composable
fun MyTheme(
isDark: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (isDark) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Preview(
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun CustomTextPreview() {
MyTheme {
Scaffold {
CustomText(
text = "Wow, a preview!",
color = MaterialColors.onBackground
)
}
}
}
My man. Lookin' good.
Multiple Previews
Another handy thing you can do with previews, is render multiple of them. Like so:
@Preview(device = Devices.NEXUS_5)
@Composable
fun CustomTextPreviewNexus5() {
...
}
@Preview(device = Devices.PIXEL_3)
@Composable
fun CustomTextPreviewPixel3() {
...
}
You can add as many previews as you like.
Composing Previews
We touched on this earlier - but remember, your Previews
are Composable
functions themselves. This means you can leverage the power of Compose, and nest your Preview
inside of other Composables
. For example, let's say you want to always render your preview in a Box
, in a Scaffold
, with a default padding of 16.dp
, and a customisable background colour:
@Composable
fun PreviewWrapper(
backgroundColor: Color = Color.Transparent,
content: @Composable () -> Unit
) {
Scaffold(
backgroundColor = backgroundColor
) { padding ->
Box(
modifier = Modifier
.padding(padding)
.padding(16.dp)
) {
content()
}
}
}
Now, let's write a Preview for each background colour we want to see:
@Preview
@Composable
fun CustomTextPreviewBlue() {
PreviewWrapper(backgroundColor = Color.Blue.copy(alpha = 0.25f)) {
CustomText(
text = "A preview with a blue background",
color = Color.White
)
}
}
@Preview
@Composable
fun CustomTextPreviewRed() {
PreviewWrapper(backgroundColor = Color.Red.copy(alpha = 0.25f)) {
CustomText(
text = "A preview with a red background",
color = Color.White
)
}
}
Neat-o!
Preview Parameters
All this preview functionality is really great, but.. a normal project contains a lot of composables, and we don't really want to have to define multiple @Preview(...)
for every possible configuration that we would like to see each Composable rendered in. There must be a better way!
Enter PreviewParameter
.
The PreviewParameter
annotation, and PreviewParameterProvider<T>
interface allow us to define a sequence of values (like a list), which are passed into a single @Preview
definition. The IDE will then generate a preview for each value in the sequence.
Dark/Light theme provider
Let's generate a preview for both the light and dark themes.
First, let's say our app's content is rendered inside of a theme Composable that looks like so:
@Composable
fun MyTheme(
isDark: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colors = if (isDark) DarkColorPalette else LightColorPalette,
content = content
...
)
}
We'll define aPreviewParameterProvider
, which returns a sequence of Boolean
, one for each Preview that will be rendered:
class DarkThemeProvider : PreviewParameterProvider<Boolean> {
override val values: Sequence<Boolean>
get() = sequenceOf(
true, // dark
false // light
)
}
Now let's pass this into our Preview:
@Preview
@Composable
fun CustomTextPreview(
@PreviewParameter(DarkThemeProvider::class) isDark: Boolean
) {
MyTheme(isDark = isDark) {
PreviewWrapper(
backgroundColor = MaterialColors.background
) {
CustomText(
text = "A preview with colors determined by the PreviewParameter",
color = MaterialColors.onBackground
)
}
}
}
We get two previews for the price of one! You could probably move MyTheme
into PreviewWrapper
, and pass isDark
to the wrapper, to reduce some of the boilerplate.
Now.. you might say that this is a lot of work just so you don't have to write a preview twice. But - we can reuse the DarkThemeProvider
in all of our other previews as well. What if our sequence returns 10 different variants? That's 10 previews you don't have to write, per Composable. Best of all, we can get really creative with which types the PreviewParameterProvider
returns.
ViewState Provider
My favourite version of this lately, is providing different ViewStates
for a Composable 'screen':
class ViewStateProvider : PreviewParameterProvider<MyViewState> {
override val values: Sequence<MyViewState>
get() = sequenceOf(
ViewState.Loading,
ViewState.Success,
ViewState.Error
)
}
@Preview
@Composable
MyScreenPreview(@PreviewParameter(ViewStateProvider::class) viewState: ViewState) {
MyScreen(viewState = viewState)
}
Get creative!
There's really no limit to this. Return a matrix of ViewState and Theme. Or, a class representing device dimensions, which renders the preview at the exact width & height you desire. Mixing the PreviewParameter with Composables which then render your content means you can really go to town.
Note: At the time of writing, there is a bug in Android Studio Chipmunk which requires the PreviewParameterProvider class to be public. This is fixed in Electric Eel.
¹ Realtime does not mean the same thing in Android Studio as it does in real life.