A preview that needs a ViewModel is telling you something
@Preview renders a composable without running the app. Its real value is the pressure it applies: a component that can't be previewed is a component that can't be tested, and both point at the same design problem.

Day 92 of 100, opening the tools pillar. Previews have been mentioned in nearly every post; this is the one about the tool itself.
The symptom
A preview that can't be written:
@Preview
@Composable
fun OrderScreenPreview() {
OrderScreen() // needs a ViewModel, which needs a repository, which needs Retrofit
}
The preview panel shows a stack trace instead of a screen. Someone adds a fake ViewModel, which needs a fake repository, which needs a fake API client — thirty lines of scaffolding to render one card.
The usual conclusion is that previews don't work for real screens.
Why the obvious fix fails
The obvious fix is to make the preview construct the graph:
@Preview
@Composable
fun OrderScreenPreview() {
OrderScreen(viewModel = FakeOrderViewModel(FakeRepo(FakeApi())))
}
It renders. It's also now coupled to the whole dependency chain, so a constructor change three layers down breaks the preview — and previews aren't compiled by CI, so it breaks silently and stays broken.
The preview didn't fail because previews are limited. It failed because OrderScreen takes
a dependency instead of data.

The actual mechanism
@Preview renders a zero-argument composable in the IDE, without running the app.
That constraint is the whole design, and it's what makes previews a design check as well as
a rendering tool.
Day 16's split is what makes it work:
@Composable
fun OrderScreen(vm: OrderViewModel = viewModel()) { // stateful, not previewable
val state by vm.uiState.collectAsStateWithLifecycle()
OrderContent(state, onRetry = vm::retry)
}
@Composable
fun OrderContent(state: OrderUiState, onRetry: () -> Unit) { … } // previewable
@Preview
@Composable
private fun OrderContentPreview() {
AppTheme { OrderContent(OrderUiState.Ready(sampleOrders), onRetry = {}) }
}
Data in, callbacks out, no graph. The same property that made Day 85's tier-2 test trivial makes the preview trivial, because they're the same requirement.
The rule: preview the stateless content composable, not the stateful screen.
Multipreview
One annotation, many renders — and defining your own is what makes it maintainable:
@Preview(name = "light", uiMode = UI_MODE_NIGHT_NO)
@Preview(name = "dark", uiMode = UI_MODE_NIGHT_YES)
@Preview(name = "large font", fontScale = 2f)
@Preview(name = "narrow", widthDp = 320)
@Preview(name = "RTL", locale = "ar")
annotation class AppPreviews
@AppPreviews
@Composable
private fun OrderCardPreviews() {
AppTheme { OrderCard(sampleOrder) }
}
Five renders from one annotation, and the set is defined once for the whole codebase. When
the team decides tablet width matters too, it's one line in AppPreviews and every
component gains the render.
Those five configurations are the ones that catch real bugs, and they're the ones the series kept returning to — dark mode contrast (Day 45), font-scale clipping (Day 46), narrow-width overflow (Day 26), RTL mirroring (Day 47).
Parameter providers
For rendering a component across its data variations rather than its configurations:
class OrderProvider : PreviewParameterProvider<Order> {
override val values = sequenceOf(
sampleOrder,
sampleOrder.copy(title = "A very long title that will certainly wrap onto three lines"),
sampleOrder.copy(status = Status.Overdue),
sampleOrder.copy(items = emptyList()),
)
}
@Preview
@Composable
private fun OrderCardVariants(@PreviewParameter(OrderProvider::class) order: Order) {
AppTheme { OrderCard(order) }
}
Day 51's point applies directly: the value is entirely in choosing data that varies. A provider of four near-identical samples renders four near-identical cards and proves nothing.
The same provider can feed screenshot tests (Day 87), which is worth doing — one definition of "the interesting cases", used by the preview and the golden images.
Interactive and animation preview
Two modes worth knowing exist:
Interactive Preview runs the composable with working state and gestures, so a toggle toggles and a list scrolls — without deploying to a device. It's the fastest loop available for a component with local state. It doesn't run coroutines against real dependencies, so a component that loads data still needs the fake — another instance of the same design pressure.
Animation Preview is Day 63's tool: it inspects labelled transitions, lets you scrub a
timeline and slow playback to 10%. It only groups animations that share a labelled
Transition, which is the practical argument for the label parameters.
Preview annotations that aren't @Preview
Two more that pull their weight:
@PreviewScreenSizes, @PreviewFontScale, @PreviewLightDark and @PreviewDynamicColors
are built-in multipreviews covering the common axes — worth using before writing your own,
and worth replacing with your own once you want a specific set.
showBackground = true is the parameter most often missing. Without it a preview renders
on transparent, so a component that assumes a surface behind it looks wrong in a way that
isn't its fault.
Two more worth knowing: heightDp/widthDp constrain the render, which is how you preview
a component in the box it will actually occupy rather than at its intrinsic size; and
group = "…" sorts previews into named tabs, which keeps a design-system file with forty
previews navigable.
The design check
The claim worth making explicitly, because it's the reason to care about previews beyond convenience:
If a component is hard to preview, it's hard to test and hard to reuse. All three need the same thing — data in, callbacks out, no ambient dependencies. A preview is the fastest of the three to attempt, so it's the cheapest way to notice the problem.
The list of things that make a preview hard is exactly the list from earlier pillars: a
NavController passed down (Day 91), a CompositionLocal used for dependencies (Day 15), a
ViewModel constructed internally (Day 12), state that can't be constructed without a
network call.
Fixing any of them improves the component for reasons unrelated to previews.
How to prove it
Take a screen and try to preview its innermost content composable with literal data. Time how long it takes.
Under a minute means the component is well-factored. If it needs fakes, or a theme wrapper
you don't have, or a CompositionLocalProvider for something that isn't a theme — that's
the design signal, and it's the same friction a test would have hit.
The other check is coverage: does every screen have a preview for its empty, error and loading states, not just its happy path? Those are the states that ship broken (Day 85), and a preview per state is the cheapest guard.
What this generalizes to
The principle is a fast feedback loop is also a design constraint. A preview renders in a second, so anything that makes it slow or impossible gets noticed immediately — where the same coupling in a test suite gets tolerated for months.
That's worth seeking out deliberately. Tools that are cheap to run apply constant, gentle pressure toward the designs that keep them cheap, and a team with good previews tends to have testable components without having argued about testability.
The corollary is a caution. Previews aren't compiled by CI in a default setup, so they rot silently — a renamed parameter breaks twenty of them and nobody notices until someone opens the file. Wiring them into a screenshot-test run (Day 87) fixes that, and turns the pressure from gentle into enforced.
Tomorrow, Day 93: Layout Inspector and the recomposition counts, read properly.
Day 92 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Previews.