Jetpack Compose Preview: Build and Test UI Faster

Quick answer: Add
@Previewto a small composable that receives sample state. Android Studio renders it beside your code, so you can inspect UI changes, themes, sizes, and states without starting an emulator. Preview accelerates visual iteration; it does not replace device testing.
Compose Preview is one of the highest-leverage habits in a Compose project. A good preview makes a component easy to inspect in isolation, catches visual regressions early, and encourages an API where state comes in through parameters and events leave through callbacks.
Android Studio refreshes a Preview as you edit the composable. Its official preview tooling guide also supports interactive previews, multi-device variants, preview parameters, and running one preview on a real device or emulator.
Your first Compose Preview
Start with a component that has explicit inputs:
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun WelcomeCard(name: String) {
Card {
Text(text = "Welcome, $name!")
}
}
@Preview(showBackground = true)
@Composable
fun WelcomeCardPreview() {
WelcomeCard(name = "Ada")
}Open the file in Android Studio and choose Split or Design. The @Preview annotation tells Studio to render WelcomeCardPreview() in the preview panel. showBackground = true is helpful when a component does not paint its own background.
Keep the preview function close to the component while it is small. For a larger feature, putting previews in a dedicated FeaturePreviews.kt file can keep production source easier to scan.
Preview the component, not the whole app route
A preview works best when it renders a visual component with simple, supplied state. This is a natural extension of the @Composable function model: data flows in, event callbacks flow out.
Avoid previewing a screen that directly creates a ViewModel, starts a repository call, or relies on navigation and dependency injection. Instead, split the screen into a route and a content composable:
@Composable
fun ProfileRoute(
viewModel: ProfileViewModel,
) {
val state = viewModel.uiState
ProfileContent(
state = state,
onRefresh = viewModel::refresh,
)
}
@Composable
fun ProfileContent(
state: ProfileUiState,
onRefresh: () -> Unit,
) {
// Render state here.
}
@Preview(showBackground = true)
@Composable
fun ProfileContentPreview() {
ProfileContent(
state = ProfileUiState(name = "Ada", isLoading = false),
onRefresh = {},
)
}The preview system cannot construct every dependency a ViewModel may require, especially when the ViewModel uses Hilt, repositories, or use cases. A stateless ProfileContent therefore previews more reliably and becomes easier to test. This same separation is useful when you later manage screen state with a ViewModel.
Always preview meaningful UI states
The most valuable preview is not the happy path alone. Add samples for loading, empty, error, and populated states—the states that are easy to forget when you only run the app with local development data.
@Preview(name = "Loading", showBackground = true)
@Composable
fun OrdersLoadingPreview() {
OrdersContent(
state = OrdersUiState.Loading,
onRetry = {},
)
}
@Preview(name = "Empty", showBackground = true)
@Composable
fun OrdersEmptyPreview() {
OrdersContent(
state = OrdersUiState.Empty,
onRetry = {},
)
}
@Preview(name = "Error", showBackground = true)
@Composable
fun OrdersErrorPreview() {
OrdersContent(
state = OrdersUiState.Error(message = "Could not load orders"),
onRetry = {},
)
}The name parameter makes each rendered tile easy to identify. Use believable sample text: a long name, a two-line title, and a realistic error message reveal problems that "Hello" often hides.
Check dark mode, text size, and locale
Preview annotations accept configuration parameters so you can find layout assumptions before users do.
import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Preview
@Preview(
name = "Dark and larger text",
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES,
fontScale = 1.3f,
locale = "es",
)
@Composable
fun WelcomeCardAccessibilityPreview() {
WelcomeCard(name = "Alexandria")
}Useful @Preview parameters include:
| Parameter | What it helps you inspect |
|---|---|
showBackground | Components without their own visible surface |
uiMode | Light and dark theme treatment |
fontScale | Text clipping, wrapping, and touch-target pressure |
locale | Translated strings and layout direction risks |
widthDp / heightDp | A constrained component or specific screen area |
device | Phone, tablet, foldable, and other reference configurations |
Do not use Preview to certify accessibility or localization. It is a quick visual check; use a device, TalkBack, automated tests, and localized QA before release.
Use MultiPreview annotations for repeatable coverage
If every component needs the same variations, stack Android’s MultiPreview annotations instead of manually duplicating previews. Current tooling includes @PreviewLightDark, @PreviewFontScale, @PreviewScreenSizes, and @PreviewDynamicColors.
import androidx.compose.ui.tooling.preview.PreviewFontScale
import androidx.compose.ui.tooling.preview.PreviewLightDark
@PreviewLightDark
@PreviewFontScale
@Composable
fun WelcomeCardMultiPreview() {
AppTheme {
WelcomeCard(name = "Ada")
}
}Each MultiPreview annotation produces its own set of variants. They are independent rather than a cross-product, so adding two annotations does not automatically generate every light/dark and font-scale combination. Use a targeted manual preview when you need one exact combination.
For a design system, you can also define a project-specific annotation that combines the variations your team cares about:
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewFontScale
import androidx.compose.ui.tooling.preview.PreviewLightDark
@PreviewLightDark
@PreviewFontScale
annotation class AppPreview
@AppPreview
@Composable
fun WelcomeCardAppPreview() {
AppTheme {
WelcomeCard(name = "Ada")
}
}Keep custom preview annotations narrow. A large grid makes the panel slow and makes important visual differences harder to see.
Provide real sample data with PreviewParameterProvider
When a component needs several data values, @PreviewParameter avoids writing separate preview functions. Create a provider whose values sequence supplies samples:
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
data class ArticleCardModel(
val title: String,
val author: String,
)
class ArticlePreviewProvider : PreviewParameterProvider<ArticleCardModel> {
override val values = sequenceOf(
ArticleCardModel(
title = "Short title",
author = "Brahim",
),
ArticleCardModel(
title = "A deliberately long title that must wrap without hiding content",
author = "Brahim Oubbad",
),
)
}
@Preview(showBackground = true)
@Composable
fun ArticleCardPreview(
@PreviewParameter(ArticlePreviewProvider::class) article: ArticleCardModel,
) {
ArticleCard(article = article)
}PreviewParameterProvider belongs to ui-tooling-preview and supplies a sequence of values that Android Studio passes to the preview. Use it for representative visual data, not as a replacement for a repository, database, or your app’s real state handling.
Add the app theme in your Preview
Preview a visual component inside the same theme wrapper used by the app. Without it, colors, typography, and shapes can look unlike the real screen.
@Preview(showBackground = true)
@Composable
fun SettingsItemPreview() {
AppTheme {
SettingsItem(
label = "Notifications",
enabled = true,
onEnabledChange = {},
)
}
}When you start building a broader Material 3 design system, make theme previews part of your component workflow. This pairs especially well with a reusable Modifier parameter and small content composables.
Interactive mode, Run Preview, and device testing
Compose Preview is more than a static screenshot:
- Interactive mode runs a preview in an isolated sandbox where you can tap controls, type input, and exercise gestures or animations quickly.
- Run Preview deploys a selected preview as an Activity on a connected device or emulator. It shares the project app’s context and permissions.
- Compose Preview remains best for focused UI iteration; use an emulator or physical device to verify navigation, permissions, system UI, configuration changes, performance, and real data behavior.
Preview annotation arguments such as widthDp, locale, and uiMode do not apply when you use Run Preview on a device. Test those configurations separately on the target device or emulator.
Preview limitations and practical fixes
Previews are lightweight because Android Studio does not launch the complete Android framework. That means no network access, no file access, and partial availability of some Context APIs.
Use one of these fixes rather than adding production-only workarounds:
- Pass sample UI state to a content composable.
- Keep data loading, navigation, and dependency injection in a route-level composable.
- Use a placeholder for preview-only images or remote data when necessary.
- If a component must behave differently while Studio inspects it, check
LocalInspectionMode.currentsparingly and keep the branch limited to preview-safe presentation data.
Avoid using LocalInspectionMode to hide a design flaw or bypass normal UI logic. A state-driven content composable is usually the cleaner fix.
A practical Preview checklist
Before considering a component visually complete, inspect:
- the normal state and every intentional empty, loading, and error state;
- light and dark themes;
- a larger font scale;
- a long string or a translated string;
- narrow and wide constraints if the component may appear in both;
- the actual screen on an emulator or device.
This small routine prevents a surprising number of UI regressions. It is especially useful for items that later appear in a LazyColumn, where a tiny item-level issue repeats across an entire screen.
FAQ
Why is my Preview blank or failing to render?
First check that the function is annotated with both @Preview and @Composable, then look for missing theme resources, a direct ViewModel or dependency-injection call, and code that performs network or file work. Reduce the preview to a content composable with supplied sample state until it renders.
Can I preview a ViewModel-backed screen?
You can sometimes supply what it needs manually, but the recommended approach is to preview the ViewModel-free content composable. The preview environment cannot reliably construct a full dependency graph.
Does Preview replace UI tests?
No. Preview checks appearance and supports quick manual interaction. Keep automated tests for behavior and use real devices or emulators for integration and platform behavior.
Next steps
Add previews to the smallest reusable components first, then create named variants for the states your product actually has. Once that workflow is comfortable, the next foundation is layout: choosing between Row, Column, and Box and verifying each at different constraints.