Blog My Happy PlaceGemma Lara Savill

Unlock MultiPreviews in Compose Multiplatform: The Super Provider Pattern

By Gemma Lara Savill
Published at January 17, 2026

Unlock

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:

  1. Copy-Pasting Android Annotations: We tried bringing the UiMode and PreviewLightDark code 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 like uiMode, fontScale, or device in the common source set.
  2. Manual Duplication: I went back to basics, creating fun AppPreviewLight() and fun 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.

Compose Multiplatform MultiPreviews in action using the Super Provider pattern

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:

  1. 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.
  2. KMP Compatible: It relies on standard Compose runtime features (CompositionLocal, PreviewParameter), so it works perfectly in the Common source set right now.
  3. Clean Code: No more wall of annotations above every function. Just one @Preview and 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:

Project Links:

You can find the sample project demonstrating this pattern on GitHub: https://github.com/GemmaLaraSavill/KMP-Multipreview