Unlock MultiPreviews in Compose Multiplatform: The Super Provider Pattern
By Gemma Lara Savill
Published at January 17, 2026
Unlock MultiPreviews in Compose Multiplatform
In my previous post, See it Before You Build It: Mastering Jetpack Compose Previews, we dove deep into mastering Jetpack Compose previews on Android. We explored how powerful tools like @PreviewLightDark, @PreviewFontScale, and @PreviewScreenSizes allow us to spot layout bugs and accessibility issues instantly, without deploying to a device.
But recently, I've been transitioning to Compose Multiplatform (KMP) to share UI logic across Android and iOS. And I noticed that MultiPreviews aren't available yet.
The "Missing" Features
The @Preview annotation exists in the common code, but it's currently a stripped-down version.
I tried a few things that failed:
- Copy-Pasting Android Annotations: We tried bringing the
UiModeandPreviewLightDarkcode over. While I could compile the annotation classes, the IDE's Preview pane simply ignored them. It doesn't yet know how to interpret parameters likeuiMode,fontScale, or device in the common source set. - Manual Duplication: I went back to basics, creating
fun AppPreviewLight()andfun AppPreviewDark(). It worked, but this solution is boilerplate-heavy and hard to maintain across dozens of screens, so I discarded this approach.
I wanted the power of MultiPreviews, one annotation to generate multiple scenarios, but working in my KMP app.
A better solution: The "Super Provider" Pattern
Since we can't rely on the IDE to automagically configure the environment (like switching the system theme), we have to do it ourselves. The key is Dependency Injection for Previews, specifically using @PreviewParameter.
I built a custom "Environment" system that mimics all those fancy Android annotations in a single, reusable provider.
Step 1: Define the Environment
First, I stopped thinking about just "Dark Mode" and started thinking about the Environment. I created a simple data class to hold the configuration of our device:
// PreviewUtils.kt
data class PreviewEnvironment(
val name: String,
val colorScheme: ColorScheme,
val screenSize: DpSize,
val fontScale: Float = 1.0f
)
Step 2: The Provider
Next, I implemented PreviewParameterProvider. This is where the magic happens. Instead of needing 4 different annotations (@PreviewFontScale, @PreviewScreenSizes, etc.), we define our test scenarios in one place:
// PreviewUtils.kt
class PreviewEnvironmentProvider : PreviewParameterProvider<PreviewEnvironment> {
override val values = sequenceOf(
// 1. Standard Phone (Light)
PreviewEnvironment("Phone Light", lightColorScheme(), DpSize(360.dp, 640.dp)),
// 2. Standard Phone (Dark) - Mimics @PreviewLightDark
PreviewEnvironment("Phone Dark", darkColorScheme(), DpSize(360.dp, 640.dp)),
// 3. Tablet - Mimics @PreviewScreenSizes
PreviewEnvironment("Tablet Light", lightColorScheme(), DpSize(800.dp, 1280.dp)),
// 4. Accessibility Check - Mimics @PreviewFontScale
PreviewEnvironment("Big Font Dark", darkColorScheme(), DpSize(360.dp, 640.dp), fontScale = 1.5f)
)
}
Step 2.5: Adapting the Theme
There's one catch. Most generated KMP themes rely entirely on isSystemInDarkTheme() to decide which colors to use. Since our IDE preview can't flip that system switch, we need to be able to inject our environment's color scheme manually.
I tweaked the AppTheme to accept an optional colorScheme parameter. If provided, it overrides the system default-perfect for our previews.
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Add this optional parameter!
colorScheme: ColorScheme? = null,
content: @Composable () -> Unit
) {
// Priority: 1. Injected Scheme (Preview) -> 2. System Setting (Real App)
val finalColorScheme = colorScheme
?: if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = finalColorScheme,
typography = Typography,
content = content
)
}
Step 3: The Universal Preview Wrapper
Finally, I needed a Composable that knows how to read this environment and apply it. I extracted this logic into a reusable Multipreview composable:
@Composable
fun Multipreview(
preview: PreviewEnvironment,
content: @Composable () -> Unit
) {
// Manually calculate density for Font Scaling
val currentDensity = LocalDensity.current
val customDensity = remember(currentDensity, preview.fontScale) {
Density(currentDensity.density, preview.fontScale * currentDensity.fontScale)
}
CompositionLocalProvider(LocalDensity provides customDensity) {
// Now we use our actual AppTheme, passing the environment color
AppTheme(colorScheme = preview.colorScheme) {
// Force the surface size to mimic the device
Surface(modifier = Modifier.size(preview.screenSize)) {
content()
}
}
}
}
Now, applying it to any screen is incredibly simple and clean:
@Composable
@Preview
private fun AppPreview(
@PreviewParameter(PreviewEnvironmentProvider::class) preview: PreviewEnvironment,
) {
Multipreview(preview) {
AppContent(Modifier.fillMaxSize())
}
}
Why this works for me
By using this pattern, I actually gained more control than I had experienced before in Android:
- Consistency: Every single screen in the app now uses the exact same test scenarios. If I were to add a "Foldable" configuration to the provider, every preview in the app gets a Foldable Preview automatically.
- KMP Compatible: It relies on standard Compose runtime features (CompositionLocal, PreviewParameter), so it works perfectly in the Common source set right now.
- Clean Code: No more wall of annotations above every function. Just one
@Previewand one parameter.
We might not have the official MultiPreviews yet, but with a little creativity, we can build something even better. Happy coding!
Related Posts:
- See it Before You Build It: Mastering Jetpack Compose Previews
- Bringing my AI news classifier to iOS with Kotlin Multiplatform
Project Links:
You can find the sample project demonstrating this pattern on GitHub: https://github.com/GemmaLaraSavill/KMP-Multipreview