A practical guide to reliable Jetpack Compose @Preview support, covering ViewModels, state hoisting, and dependency injection, reproducible from scratch.
Jetpack Compose previews are one of the best productivity features to land in Android development in years, when they actually work. The moment a composable pulls in a ViewModel that expects a Hilt or Koin graph, or reaches for a Context that assumes a running Activity, the preview render fails with a stack trace that tells you almost nothing useful. Most teams respond by giving up on previewing anything past the leaf components, which throws away most of the value the feature was supposed to provide in the first place.
The underlying cause is almost always the same one. A composable is written to fetch its own data by calling hiltViewModel() or viewModel() directly, instead of receiving already-resolved state as a plain parameter. The preview renderer executes your composable in an isolated environment with no dependency graph, no Activity, and no real Application context, so anything that reaches outside the function to pull its own dependencies is going to fail before a single pixel is drawn.
The fix is a discipline more than a library. A composable that is meant to be previewable should take its entire visual state as parameters, plain data classes and lambdas, and should never resolve a ViewModel, a repository, or a use case from inside its own body. The ViewModel resolution happens one level up, in the screen-level composable that is wired into navigation, and that screen-level composable is the one thing you accept you cannot preview directly. Everything below it can be previewed freely, because everything below it only knows about data, not about where that data came from.
This is the same state-hoisting principle Compose already encourages for testability and reuse. Preview-friendliness turns out to be almost a free side effect of following it properly, which is a good sign that it is the right pattern rather than an awkward workaround.
hiltViewModel(), collects state with collectAsStateWithLifecycle(), and passes plain state plus event lambdas down. Not previewable, and that is fine.Sometimes a screen genuinely needs to preview with a ViewModel attached, usually because the ViewModel exposes more than simple state, such as a `SavedStateHandle`-backed value or derived flows you want to exercise in the preview itself. In that case, do not fight Hilt inside a preview. Construct the ViewModel manually with fake dependencies instead.
Define a small, in-memory fake for each repository or use case the ViewModel depends on, living in your `debug` or `androidTest` source set so it never ships to production. The fake returns canned data synchronously or via a `MutableStateFlow` you control, and the preview constructs the ViewModel directly with `remember { MyViewModel(fakeRepository) }` rather than asking Hilt to do it. This keeps the preview fast, deterministic, and completely detached from your real dependency graph, which is exactly what you want when you are iterating on layout and do not want an actual network fake to slow down every recomposition.
A single preview showing the happy path tells you almost nothing about how a screen behaves under load, on error, or when empty. `PreviewParameterProvider` lets you drive a single preview function across several states without writing a separate function for each one.
PreviewParameterProvider<ScreenState> whose values sequence yields one instance per state.@PreviewParameter(ScreenStateProvider::class) state: ScreenState on the composable's preview wrapper, and Android Studio renders every state side by side in the preview pane.This one change catches a disproportionate share of real bugs before a device ever runs the app, because empty and error states are exactly the ones engineers forget to visually check, and exactly the ones that end up looking broken in production because nobody looked at them until a user hit one by accident.
Compose's multipreview annotations let you define a single custom annotation that bundles several `@Preview` configurations, then apply that one annotation to any composable to render it across all of them at once. A typical bundle covers a phone and a tablet width, light and dark theme, and a larger system font scale, since font-scale bugs in particular are the ones that almost never get caught any other way. Once the annotation exists, adding full device and accessibility coverage to a new screen costs one line rather than eight separate preview functions, which is the difference between this actually happening and it quietly not happening after the second sprint.
stringResource() and friends, which work correctly inside previews, over manual LocalContext.current.resources calls.NavController threaded three levels deep into a content composable makes that composable unpreviewable and hard to test. Pass a lambda like onItemClick: (String) -> Unit instead, and let the route composable own the actual navigation call.rememberCoroutineScope() and immediately call a suspend function on a real repository will crash previews the moment that repository expects a live network or database connection. Keep suspend calls behind the same fake boundary as everything else.System.currentTimeMillis(), causes previews to shift slightly on every recomposition and makes visual diffs in code review noisy. Use fixed, hardcoded sample data for anything that appears in a preview.None of this is exotic engineering. It is mostly the discipline of keeping composables honest about what they depend on, which happens to be the exact same discipline that makes them fast to preview, easy to test, and much less likely to surprise you the first time a real device runs them. We cover this kind of Android platform work, previews included, as part of our mobile engineering practice.
Before we start, please share a few details so we can follow up with you.
End this conversation? Your chat will be emailed to us.