Compose Previews Without the Boilerplate
Practical preview patterns for catching UI bugs before QA ever sees them
Compose Previews Without the Boilerplate
Practical preview patterns for catching UI bugs before QA ever sees them

Most Compose projects start the same way.
You write a composable, slap a single @Preview on top of it, glance at the result, and move on.
Then reality happens.
A German translation suddenly wraps into three lines. A 200% font scale destroys the layout. Dark mode makes text unreadable. A smaller device pushes everything off-screen.
One preview is rarely enough.
The good news is that Compose already gives us the tools to preview components across multiple configurations without duplicating preview functions everywhere.
This article covers the patterns I actually use in real projects: custom preview annotations, stacked previews, data-driven previews, theme variants, and keeping previews maintainable as the codebase grows.
No preview spam. No copy-paste hell.
The problem with “just one preview”
A single @Preview gives you exactly one snapshot of a composable.
That’s fine for simple components.
But UI bugs usually appear at the edges, larger accessibility font scales, long localized strings, tiny devices, dark mode, empty states, and overloaded content.
The naive solution is creating a separate preview function for every variation.
@Preview
@Composable
private fun ButtonPreview() { }
@Preview(fontScale = 1.3f)
@Composable
private fun ButtonLargeFontPreview() { }
@Preview(locale = "de")
@Composable
private fun ButtonGermanPreview() { }
This works … until the component changes and now you have 8 previews to maintain.
Compose has a much cleaner solution.
Custom preview annotations
Compose supports a pattern commonly called “MultiPreview”.
Instead of duplicating preview functions, you create your own annotation that groups multiple @Preview annotations together.
@Preview(name = "Small font", fontScale = 0.85f)
@Preview(name = "Default font", fontScale = 1.0f)
@Preview(name = "Large font", fontScale = 1.3f)
@Preview(name = "Huge font", fontScale = 2.0f)
annotation class FontScalePreviews
Now you can apply all of them with a single annotation:
@FontScalePreviews
@Composable
private fun MyButtonPreview() {
AppTheme {
MyButton(text = "Submit")
}
}
The IDE automatically renders four previews.
Add another scale later, and every composable using that annotation gets updated automatically.
This is one of those small patterns that scales surprisingly well across large projects.
Useful preview annotations
Font scale previews
Accessibility issues appear fast once users increase system font size.
@Preview(name = "85%", fontScale = 0.85f)
@Preview(name = "100%", fontScale = 1.0f)
@Preview(name = "130%", fontScale = 1.3f)
@Preview(name = "200%", fontScale = 2.0f)
annotation class FontScalePreviews
The 130% and 200% variants usually expose clipped text, broken rows, incorrect weights, and layouts that assumed fixed heights.
If you only keep one custom preview annotation in your project, keep this one.
Device size previews
A layout that looks perfect on a Pixel 9 Pro can completely collapse on smaller devices.
@Preview(name = "Compact phone", device = "spec:width=320dp,height=568dp,dpi=320")
@Preview(name = "Standard phone", device = "spec:width=375dp,height=667dp,dpi=320")
@Preview(name = "Large phone", device = "spec:width=412dp,height=915dp,dpi=420")
@Preview(name = "Foldable", device = "spec:width=673dp,height=841dp,dpi=320")
@Preview(name = "Tablet", device = "spec:width=800dp,height=1280dp,dpi=240")
annotation class DeviceSizePreviews
This is especially useful for screen-level composables, onboarding flow, forms, dashboards, and anything using adaptive layouts.
The 320dp preview is still incredibly valuable.
A surprising number of layout bugs only appear once horizontal space becomes painful.
Light and dark mode
import android.content.res.Configuration
@Preview(
name = "Light",
uiMode = Configuration.UI_MODE_NIGHT_NO,
showBackground = true,
)
@Preview(
name = "Dark",
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
)
annotation class ThemeModePreviews
Simple. Effective.
You’ll instantly catch invisible text, missing surface colors, incorrect alpha usage, and hardcoded colors.
If your app doesn’t support dark mode, skip this entirely.
No reason to maintain previews nobody cares about.
Locale previews
Localized text breaks layouts far more often than people expect.
@Preview(name = "EN", locale = "en")
@Preview(name = "DE", locale = "de")
@Preview(name = "TR", locale = "tr")
annotation class LocalePreviews
German and Turkish are especially good at exposing layout assumptions because words tend to become longer.
This catches overflow, truncation, broken button widths, and awkward wrapping before QA finds it.
RTL previews
@Preview(name = "LTR")
@Preview(name = "RTL", locale = "ar")
annotation class LayoutDirectionPreviews
This is the easiest way to verify your layout works correctly in RTL mode.
It quickly exposes mistakes like the following:
padding(start = 16.dp)
when the layout should have used symmetrical spacing or directional modifiers.
Combining annotations
Annotations can stack.
@FontScalePreviews
@LocalePreviews
annotation class AccessibilityPreviews
This creates 4 font scales across 3 locales for a total of 12 previews.
Powerful. But dangerous.
Preview counts explode quickly.
Use combinations intentionally.
Most composables do not need every possible configuration.
Previewing multiple content states
Custom preview annotations are great for environment changes.
But what about content itself?
Examples:
- empty state
- loading state
- long names
- missing data
- overloaded content
That’s where it PreviewParameterProvider becomes useful.
class UserCardSamples : PreviewParameterProvider<UserCardUiModel> {
override val values = sequenceOf(
UserCardUiModel(
name = "John Doe",
email = "john@company.com",
),
UserCardUiModel(
name = "Very-Very-Long-Name-That-Breaks-Layouts",
email = "long.email.address@example.com",
),
UserCardUiModel(
name = null,
email = null,
),
)
}
Then:
@Preview(showBackground = true)
@Composable
private fun UserCardPreview(
@PreviewParameter(UserCardSamples::class)
data: UserCardUiModel,
) {
AppTheme {
UserCard(data)
}
}
This gives you multiple previews from a single composable.
In practice, this is one of the best ways to test edge cases visually.
One annoying limitation
@PreviewParameter does not combine nicely with MultiPreview annotations.
So this:
@FontScalePreviews
@Composable
private fun Preview(
@PreviewParameter(UserCardSamples::class)
data: UserCardUiModel,
)
won’t behave the way you’d hope.
The workaround is usually simple:
Create a wrapper composable that renders multiple states inside a Column, then apply the MultiPreview annotation to the wrapper.
Not ideal. But workable.
Theme variants
If you’re building a white-label app or SDK, previews become trickier.
Different clients may inject entirely different themes.
In that scenario, annotations alone are not enough because theme selection happens through composition.
A wrapper composable works much better:
@Composable
private fun ThemeVariantsPreview(
content: @Composable () -> Unit,
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
BlueTheme { content() }
GreenTheme { content() }
DarkTheme { content() }
}
}
@Preview(showBackground = true)
@Composable
private fun MyButtonPreview() {
ThemeVariantsPreview {
MyButton(text = "Submit")
}
}
This lets you compare multiple themes side-by-side without duplicating preview functions.
Extremely useful in SDK work.
Organizing preview annotations
Once you start using these patterns consistently, preview utilities deserve their own package.
Something like:
core/ui/preview/
├── FontScalePreviews.kt
├── DeviceSizePreviews.kt
├── LocalePreviews.kt
├── ThemeModePreviews.kt
└── AccessibilityPreviews.kt
Keep them tiny.
Each file should ideally contain:
- one annotation
- minimal imports
- zero logic
Treat them like reusable UI tooling.
Performance considerations
Every @Preview is a full Compose render.
That matters.
If you aggressively combine themes, locales, font scales, device sizes, and content states you can easily end up with 40+ previews for a single composable.
Android Studio will feel it.
My rule:
Use heavy preview combinations only for the following:
- screen-level composables
- complex layouts
- accessibility-sensitive screens
Buttons, chips, and tiny components usually need just one or two previews.
Not twelve.
A few practical rules
Keep preview state deterministic
Avoid things like:
System.currentTimeMillis()
Random.nextInt()
inside previews.
Previews should be stable and predictable.
Hardcoded sample data is boring … and exactly what you want.
Keep previews private
@Preview
@Composable
private fun Preview() {}
There’s rarely a reason for preview functions to be public.
They’re tooling helpers, not production API.
Preview real edge cases
The most valuable previews are usually absurdly long strings, empty states, accessibility font sizes, constrained layouts, missing images, or loading placeholders.
Not the happy path.
The happy path almost always works.
Recommended starter setup
For most projects, you honestly only need three things:
FontScalePreviewsDeviceSizePreviewsLocalePreviews
Those alone catch a massive amount of UI regressions.
Add dark mode previews if the app supports them.
Everything else can wait until a real bug forces the need.
That’s usually the right moment to add more complexity.
Final thoughts
Compose previews become significantly more useful once you stop treating them as a single static screenshot.
The goal isn’t generating dozens of pretty preview tiles.
The goal is catching UI problems before they escape into QA or production.
Custom preview annotations are one of the simplest ways to scale that process across an entire codebase.
Minimal code. Minimal maintenance.
메타데이터
- post_id
- cd974437a4ad
- slug
- compose-previews-without-the-boilerplate-cd974437a4ad
- url
- https://blog.stackademic.com/compose-previews-without-the-boilerplate-cd974437a4ad
- canonical_url
- https://blog.stackademic.com/compose-previews-without-the-boilerplate-cd974437a4ad
- author_url
- https://medium.com/@ttkalcevic
- status
- ok
- fetched_at
- 2026-06-10 18:44:10