One Preview to Rule Them All: Unified Compose Previews in Common Code
Published on June 24, 2026
Until late 2025, managing UI previews in a Compose Multiplatform (CMP) project felt like navigating a minefield of compromises. The ecosystem was fragmented, and achieving a seamless "write once, see everywhere" experience was surprisingly difficult.
The Fragmented Past of CMP Previews¶
Back in the dark ages, there were three distinct ways to handle previews:
androidx.compose.ui.tooling.preview.Preview: the standard forandroidMain, well-supported by Android Studio and IntelliJ IDEA, requiringcompose.previewin your dependencies;androidx.compose.desktop.ui.tooling.preview.Preview: specific todesktopMain, supported by IDEs via a plugin with minimal build setup;org.jetbrains.compose.ui.tooling.preview.Preview: the common option that worked across all source sets, but with limited IDE support (mostly restricted to Fleet), requiringcompose.components.uiToolingPreviewincommonMain.
For my workflow, none of these were perfect. I never found Fleet to be a viable daily driver due to its early-stage stability and uncertain pricing model, so option #3 was out. Since my focus was primarily on mobile, the desktop-only preview (#2) wasn't of much help either.
That left me with the Android-specific annotation (#1). However, since this annotation was only
recognized within the androidMain source set and all my Composables lived in commonMain, I
would have been forced to maintain previews in separate files. Worse yet, I couldn't see them unless
I kept the Android counterpart open in a parallel pane.
So I decided to wait for the ecosystem to mature. After all, the friction only affected my Developer Experience, end-users were not affected in any way.
The Turning Point¶
The game changed with the release of Compose Multiplatform 1.10.0. This version introduced a
unified androidx.compose.ui.tooling.preview.Preview annotation that can be used directly in
commonMain and is fully recognized by both Android Studio and IntelliJ IDEA.
Of course, a unified experience requires some intentional configuration. To get this working some steps are needed:
- add the
org.jetbrains.compose.ui:ui-tooling-previewdependency to yourcommonMainsource set; - if you apply the
com.android.kotlin.multiplatform.libraryplugin, you must ensureorg.jetbrains.compose.ui:ui-toolingis available in theandroidRuntimeClasspath.
In my project, I manage configuration through a convention plugin. I had to adjust my
KotlinMultiplatformAndroidLibraryExtension setup to inject the necessary runtime dependency:
class ComposeMultiplatformPlugin : Plugin<Project> {
override fun apply(target: Project): Unit =
with(target) {
extensions.configure(KotlinMultiplatformExtension::class.java) {
targets.withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
.configureEach {
apply<KotlinMultiplatformAndroidLibraryExtension> {
dependencies.add(
"androidRuntimeClasspath",
libs.findLibrary("compose-ui-tooling").get(),
)
}
}
}
}
}
The Resource Roadblock¶
I thought I was in the clear. But as it turns out, I had only scratched the surface. As soon as I started adding the first preview, I realized that my previous architectural decisions regarding resource management were blocking the way.
My UI relied heavily on CompositionLocals for drawables and localized strings. Furthermore,
resource access was abstracted away through interfaces (CoreResources and Strings), with
concrete implementations (DefaultCoreResources and DefaultStrings) locked inside the
:shared subproject.1
This architecture was a relic of an era before native Compose resources, back when I used Lyricist and moko-resources. This setup had remained incredibly flexible though, and I wasn't ready to abandon it just for the sake of previews.
To preserve both subproject-specific previews and clean encapsulation, I performed a targeted
refactor. I moved the concrete resource adapters from :shared to their respective modules
(:core:resources and :core:l10n). I also exposed the DI bindings via public modules
(resourceModule and l10nModule), which were previously internal in :shared.
Eventually, I created an extension function in :core:commonui:components to factor out the DI
setup
for previews:
@Composable
fun RootDI.SetupPreview(vararg modules: DI.Module) = remember {
di = DI {
importAll(resourcesModule, l10nModule, *modules)
}
}
This refactoring didn't just give me functional previews in common code; it also allowed me to eliminate redundant test doubles for resources. Because my unit tests now have access to the actual resource adapters within their subprojects, the tests are both simpler and more realistic.
Closing Thoughts¶
Unified previews have bridged the gap between common code and visual feedback, making the
"multiplatform" part of Compose truly first-class. In the future, I'll be adding more of them
(as of now, I concentrated on :core:commonui:components as I was experimenting): it makes
it easier to maintain the project and spot bugs earlier.
In my case, I believe waiting until a mature solution emerged was a winning strategy. Balancing trade-offs between functionality and complexity is worth if it improves UX, but here it only involved DX (and this is mostly a one-man project); so it could be put off with no impact.
Finally, as always, adopting new parts of a technology was an opportunity for me to improve code architecture, remove obsolete workarounds and cleanup boilerplate. And this is one of the main reasons I work on this project: learn new things, refactor, improve code quality and ultimately myself as a developer.
-
In this context "subproject" and "module" can not be used interchangeably. From now on, " subproject" will refer to a build configuration unit, "module" will refer to a DI configuration unit. ↩