Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Practical Tips and Tricks
for Working with 
Com...

Practical Tips and Tricks
for Working with 
Compose Multiplatform Previews (mDevCamp 2025)

Compose Multiplatform is a game-changer for sharing UI implementation across Android, iOS, desktop, and web, but things can still get tricky when it comes to previewing those UIs.
Jetpack Compose's tooling in Android Studio is truly powerful, but what about multiplatform Composables? Can we quickly test and visualize our UIs for multiple platforms without constantly switching tools? Are previews really important now that Compose Hot Reload is emerging?

In this talk, we’ll dig into the challenges we can face when working with Compose Multiplatform Previews, from platform quirks to tooling features and shortcomings. I’ll share some practical workarounds to help you stay productive, plus tips for structuring your code to make previewing across platforms smoother. We’ll also take a look at where the tooling is headed and what we can all hope for in the future.

Expect examples and tips you can start using right away. Whether you’re just getting started or you’ve been using Compose Multiplatform for a while, this talk will give you the confidence to work more effectively with Compose Multiplatform Previews.

Avatar for István Juhos

István Juhos

June 03, 2025
Tweet

More Decks by István Juhos

Other Decks in Programming

Transcript

  1. @Preview @Composable private fun TitleSlidePreview() { PreviewColumn { TitleSlide() }

    } @Composable private fun TitleSlide() { Column( modifier = Modifier .fillMaxSize() .paint( painter = painterResource( id = R.drawable.title_image, ), contentScale = ContentScale.FillBounds, ) .padding(32.dp) ) { Title( modifier = Modifier .fillMaxWidth() .padding(top = 32.dp), fontSize = 70.sp, ) Column( horizontalAlignment = CenterHorizontally, modifier = Modifier .fillMaxWidth() ) { Name( modifier = Modifier .weight(1f) .padding(bottom = 66.dp), imageHeight = 66.dp, textSize = 70.sp, ) Contacts( modifier = Modifier .padding( bottom = 16.dp, end = 16.dp, ), fontSize = 50.sp, ) } } } Practical Tips and Tricks for Working with Compose Multiplatform Previews István Juhos @istvanjuhos.dev
  2. @istvanjuhos.dev #mDevCamp Agenda • State of the CMP Preview tooling

    • Annotation limitations • Custom-built preview tooling • Demo
  3. @istvanjuhos.dev #mDevCamp The state of CMP tooling 💧 jetbrains.com/ fl

    eet blog.jetbrains.com/kotlin/2025/02/kotlin-multiplatform-tooling-shifting-gears
  4. @istvanjuhos.dev #mDevCamp The state of CMP tooling 💧 jetbrains.com/ fl

    eet blog.jetbrains.com/kotlin/2025/02/kotlin-multiplatform-tooling-shifting-gears
  5. @istvanjuhos.dev #mDevCamp The state of CMP tooling 💧 jetbrains.com/ fl

    eet blog.jetbrains.com/kotlin/2025/02/kotlin-multiplatform-tooling-shifting-gears
  6. @istvanjuhos.dev #mDevCamp The state of CMP tooling 💧 jetbrains.com/ fl

    eet blog.jetbrains.com/kotlin/2025/02/kotlin-multiplatform-tooling-shifting-gears
  7. @istvanjuhos.dev #mDevCamp The state of CMP tooling Copyright © 2025

    JetBrains s.r.o. IntelliJ IDEA and the IntelliJ IDEA logo are trademarks of JetBrains s.r.o. Narwhal 2025.1.1
  8. @istvanjuhos.dev #mDevCamp The state of CMP tooling Copyright © 2025

    JetBrains s.r.o. IntelliJ IDEA and the IntelliJ IDEA logo are trademarks of JetBrains s.r.o. 2025.1.1 💜
  9. @istvanjuhos.dev #mDevCamp Compose Multiplatform for Desktop IDE Support • Basic

    previews in modules targeting the JVM • it’s already in maintenance mode
  10. @istvanjuhos.dev #mDevCamp Kotlin Multiplatform plugin (Beta) • Starting with IDEA

    2025.1.1 and AS Narwhal • Only on 🍎 for now, 🪟 and 🐧 coming soon
  11. @istvanjuhos.dev #mDevCamp androidx.compose.ui.tooling.preview.Preview • To be used in Android source

    sets • Supported in Android Studio / IDEA via the Android plugin
  12. @istvanjuhos.dev #mDevCamp org.jetbrains.compose.ui.tooling.preview.Preview • Introduced in CMP 1.6.0, initially for

    Fleet • To usable anywhere where declaring composables is possible • Supported in Android Studio / IDEA via the • Compose Multiplatform for Desktop IDE Support plugin • Kotlin Multiplatform plugin
  13. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In an

    Android source set @Preview @Composable fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  14. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In an

    Android source set @Preview @Composable fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } } 🤔
  15. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In an

    Android source set @androidx.compose.ui.tooling.preview.Preview @Composable fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  16. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In an

    Android source set @androidx.compose.ui.tooling.preview.Preview( showBackground = true, ) @Composable fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  17. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In an

    Android source set @androidx.compose.ui.tooling.preview.Preview( showBackground = true, ) @Composable fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  18. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview @MustBeDocumented @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.ANNOTATION_CLASS,

    AnnotationTarget.FUNCTION) @Repeatable annotation class Preview( ... val showBackground: Boolean = false, val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, ... )
  19. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview @MustBeDocumented @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.ANNOTATION_CLASS,

    AnnotationTarget.FUNCTION) @Repeatable annotation class Preview( . val showBackground: Boolean = false, val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, . )
  20. @istvanjuhos.dev #mDevCamp @androidx.compose.ui.tooling.preview.Preview( showBackground = true, ) @Composable private fun

    PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } } Compose Preview vs. CMP Preview • In an Android source set
  21. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In a

    common source set @androidx.compose.ui.tooling.preview.Preview( showBackground = true, ) @Composable private fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  22. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In a

    common source set @org.jetbrains.compose.ui.tooling.preview.Preview( showBackground = true, ) @Composable private fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  23. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In a

    common source set @org.jetbrains.compose.ui.tooling.preview.Preview( showBackground = true, ) @Composable private fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  24. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview • In a

    common source set @org.jetbrains.compose.ui.tooling.preview.Preview( showBackground = true, ) @Composable private fun PreviewListItem() { AppTheme { ListItem( text = "List item", onClick = {}, ) } }
  25. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview @MustBeDocumented @Retention(AnnotationRetention.BINARY) @Target(

    AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION ) @Repeatable annotation class Preview
  26. @istvanjuhos.dev #mDevCamp Compose Preview vs. CMP Preview @MustBeDocumented @Retention(AnnotationRetention.BINARY) @Target(

    AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION ) @Repeatable annotation class Preview No parameters 🙁
  27. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { Column

    { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build a preview
  28. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { Column

    { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build a preview
  29. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { Column

    { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build a preview
  30. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { Column

    { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build our first preview tool
  31. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { PreviewColumn

    { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build our first preview tool
  32. @istvanjuhos.dev #mDevCamp @Composable fun PreviewColumn( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { Column( modifier = Modifier .background(MaterialTheme.colorScheme.background) .padding(if (paddingEnabled) 8.dp else 0.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { content() } } Let’s build our first preview tool
  33. @istvanjuhos.dev #mDevCamp @Composable fun PreviewColumn( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { Column( modifier = Modifier .background(MaterialTheme.colorScheme.background) .padding(if (paddingEnabled) 8.dp else 0.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { content() } } Let’s build our first preview tool
  34. @istvanjuhos.dev #mDevCamp @Composable fun PreviewColumn( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { Column( modifier = Modifier .background(MaterialTheme.colorScheme.background) .padding(if (paddingEnabled) 8.dp else 0.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { content() } } Let’s build our first preview tool
  35. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { PreviewColumn

    { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build our first preview tool
  36. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { PreviewColumn

    { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build our first preview tool
  37. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { PreviewColumn(

    paddingEnabled = true ) { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build our first preview tool
  38. @istvanjuhos.dev #mDevCamp @Preview @Composable fun PreviewPrimaryButton() { AppTheme { PreviewColumn(

    paddingEnabled = true ) { PrimaryButton( ... ) PrimaryButton( ... ) } } } Let’s build our first preview tool
  39. @istvanjuhos.dev #mDevCamp Let’s build another preview tool @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION)

    @Preview(name = "Light") @Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES ... ) annotation class PreviewLightDark
  40. @istvanjuhos.dev #mDevCamp @Composable fun PreviewLightDark( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { } Let’s build another preview tool
  41. @istvanjuhos.dev #mDevCamp @Composable fun PreviewLightDark( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { Column { AppTheme(darkTheme = false) { PreviewColumn(paddingEnabled, content) } AppTheme(darkTheme = true) { PreviewColumn(paddingEnabled, content) } } } Let’s build another preview tool
  42. @istvanjuhos.dev #mDevCamp @Composable fun PreviewLightDark( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { Column { AppTheme(darkTheme = false) { PreviewColumn(paddingEnabled, content) } AppTheme(darkTheme = true) { PreviewColumn(paddingEnabled, content) } } } Let’s build another preview tool
  43. @istvanjuhos.dev #mDevCamp @Composable fun PreviewLightDark( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { Column { AppTheme(darkTheme = false) { PreviewColumn(paddingEnabled, content) } AppTheme(darkTheme = true) { PreviewColumn(paddingEnabled, content) } } } Let’s build another preview tool
  44. @istvanjuhos.dev #mDevCamp @Composable fun PreviewLightDark( paddingEnabled: Boolean = false, content:

    @Composable ColumnScope.() -> Unit, ) { Column { AppTheme(darkTheme = false) { PreviewColumn(paddingEnabled, content) } AppTheme(darkTheme = true) { PreviewColumn(paddingEnabled, content) } } } Let’s build another preview tool
  45. @istvanjuhos.dev #mDevCamp @PreviewLightDark @PreviewScreenSizes @PreviewFontScale @PreviewScreenSizes @PreviewDynamicColors @Preview( name =

    "Example preview", group = "Example preview group", showBackground = true, backgroundColor = 0xFF_E7ECF4, uiMode = UI_MODE_NIGHT_YES, widthDp = 200, heightDp = 200, fontScale = 2f, showSystemUi = true, device = Devices.DEFAULT, locale = "iw", wallpaper = Wallpapers.NONE, )
  46. @istvanjuhos.dev #mDevCamp Override CompositionLocals @Composable fun PreviewWithSettings( content: @Composable ColumnScope.()

    -> Unit, ) { var isRtl by remember { mutableStateOf(false) } ... CompositionLocalProvider( LocalLayoutDirection provides if (isRtl) { LayoutDirection.Rtl } else { LayoutDirection.Ltr }, ) { Column { content() } } }
  47. @istvanjuhos.dev #mDevCamp Override CompositionLocals @Composable fun PreviewWithSettings( content: @Composable ColumnScope.()

    -> Unit, ) { ... Column { Switch( checked = isRtl, onCheckedChange = { isRtl = it }, ) content() } }
  48. @istvanjuhos.dev #mDevCamp Override CompositionLocals @Composable fun PreviewWithSettings( content: @Composable ColumnScope.()

    -> Unit, ) { ... Column { Switch( checked = isRtl, onCheckedChange = { isRtl = it }, ) content() } }
  49. @istvanjuhos.dev #mDevCamp Override CompositionLocals @Composable fun PreviewWithSettings( content: @Composable ColumnScope.()

    -> Unit, ) { ... Column { Switch( checked = isRtl, onCheckedChange = { isRtl = it }, ) content() } }
  50. @istvanjuhos.dev #mDevCamp Override CompositionLocals @Composable fun PreviewWithSettings( content: @Composable ColumnScope.()

    -> Unit, ) { ... Column { Switch( checked = isRtl, onCheckedChange = { isRtl = it }, ) content() } }
  51. Practical Tips and Tricks for Working with Compose Multiplatform Previews

    • The CMP preview tooling is evolving quickly • Build custom reusable tools, go as wild as needed • Use the desktop build target to supercharge CMP UI development • Try Compose Hot Reload istvanjuhos.dev István Juhos