Preview-Driven Development (PDD) workflow for KMP: write Content composables first, iterate on Desktop JVM previews (3-5x faster than Android), cover all states with @PreviewParameterProvider, then promote previews directly to Roborazzi screenshot tests.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add ronjunevaldoz/kmp-agent-skills --skill kmp-compose-preview-driven-development --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Kmp Compose Preview Driven Development?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ronjunevaldoz-kmp-compose-preview-driven-development)More formats (shields.io, HTML) on the badges page.
---
name: kmp-compose-preview-driven-development
description: >
Preview-Driven Development (PDD) workflow for KMP: write Content composables first,
iterate on Desktop JVM previews (3-5x faster than Android), cover all states with
@PreviewParameterProvider, then promote previews directly to Roborazzi screenshot tests.
license: Apache-2.0
metadata:
author: kmp-agent-skills
last-updated: '2026-07-08'
keywords:
- PDD
- preview-driven development
- Desktop preview
- '@Preview'
- PreviewParameterProvider
- Compose preview
- KMP
- JVM preview
- fast iteration
- Roborazzi
---
## When to Use This Skill
Use when you need to:
- Build a new Compose UI screen or component
- Iterate quickly on layout and state variations without running a build
- Cover loading/error/empty/success states visually before writing logic
- Set up `@PreviewParameterProvider` to test all states in one function
- Create a workflow where previews double as Roborazzi golden screenshots
**Trigger keywords:** preview, PDD, desktop preview, @Preview, PreviewParameterProvider,
fast UI iteration, Compose preview, desktop app run, JVM preview, preview workflow.
**Freshness rule:** Compose Multiplatform desktop preview support evolves — recheck the
IntelliJ/Android Studio plugin version needed for interactive Desktop previews.
---
## Recommendation First
Default to **Desktop-first previews: write the `Content` composable, add `@Preview` functions
targeting the Desktop JVM target, iterate in the IDE without running a build**.
Make previews mandatory, not optional:
- every `*Content.kt` should have at least one preview stub
- feature UI modules should keep preview files alongside the screen/content code or in a
`previews/` sibling folder
- the preview should exercise the states that matter for the screen, not just the happy path
Why Desktop over Android previews:
- JVM only — no `processDebugResources`, no D8, no manifest merge
- 3–5× faster compilation than the Android target
- Works without a connected device or running emulator
- Same JVM target as Roborazzi screenshot tests — golden images are stable across machines
- `./gradlew :desktopApp:run` gives a live interactive window during development
The Screen/Content split (from `kmp-presenter-module`) is the technical prerequisite:
`Content` accepts state as a parameter, `Screen` holds the ViewModel. Previews only need `Content`.
---
## PDD Cycle
```
1. Define UiState variants (Loading / Error / Empty / Success)
↓
2. Write Content composable — accepts UiState as parameter
↓
3. Write @Preview functions for each state (or use @PreviewParameterProvider)
↓
4. Open preview panel in IDE — iterate on layout without any build
↓
5. Run ./gradlew :desktopApp:run for an interactive live window
↓
6. Promote to Roborazzi — same @Preview functions become golden screenshots
↓
7. CI runs ./gradlew :desktopApp:jvmTest — screenshot diffs fail the build
```
Steps 1–5 happen before any logic is wired — UI shape is validated visually first.
---
## Content Composable Pattern
```kotlin
// :feature:auth:ui — AuthContent.kt
@Composable
fun AuthContent(
state: AuthUiState,
onIntent: (AuthUiIntent) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxSize()) {
when (state) {
AuthUiState.Loading -> CircularProgressIndicator(Modifier.align(Alignment.Center))
is AuthUiState.Success -> UserProfile(user = state.user)
is AuthUiState.Error -> ErrorView(
message = state.message,
onRetry = { onIntent(AuthUiIntent.Retry) },
)
}
}
}
```
Key rules:
- `Content` has no `ViewModel` parameter — it receives state and a lambda
- All user actions go through `onIntent` — one callback, not N individual lambdas
- `modifier` parameter is the last named arg — standard Compose convention
---
## Preview Functions
### Per-state previews (simple)
```kotlin
@Preview
@Composable
private fun AuthContentLoadingPreview() {
AppTheme {
AuthContent(state = AuthUiState.Loading, onIntent = {})
}
}
@Preview
@Composable
private fun AuthContentSuccessPreview() {
AppTheme {
AuthContent(state = AuthUiState.Success(PreviewData.user), onIntent = {})
}
}
@Preview
@Composable
private fun AuthContentErrorPreview() {
AppTheme {
AuthContent(state = AuthUiState.Error("Session expired"), onIntent = {})
}
}
```
### `@PreviewParameterProvider` (preferred for many states)
```kotlin
class AuthStateProvider : PreviewParameterProvider<AuthUiState> {
override val values = sequenceOf(
AuthUiState.Loading,
AuthUiState.Success(PreviewData.user),
AuthUiState.Error("Session expired"),
)
}
@Preview
@Composable
private fun AuthContentAllStatesPreview(
@PreviewParameter(AuthStateProvider::class) state: AuthUiState,
) {
AppTheme {
AuthContent(state = state, onIntent = {})
}
}
```
One function, all states — the IDE renders each provider value as a separate frame.
---
## Preview Data Object
Centralise preview data so all previews use consistent models:
```kotlin
// :core:ui or :feature:<name>:ui
object PreviewData {
val user = User(
id = "preview-user-id",
name = "Alex Preview",
email = "alex@example.com",
)
}
```
Keep `PreviewData` in the `:ui` module — it is not needed outside previews.
---
## Desktop Run Cycle
```bash
# Fast iterative preview — compiles only Desktop/JVM, no AGP
./gradlew :desktopApp:run
# Headless check — all Desktop compilations succeed
./gradlew :desktopApp:compileKotlinJvm
```
Configure the Desktop entry point in `desktopApp/src/jvmMain/kotlin/main.kt`:
```kotlin
fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "Preview Runner") {
// Wire a preview composable here for live preview during development
AuthContent(state = AuthUiState.Success(PreviewData.user), onIntent = {})
}
}
```
---
## Promote Previews to Roborazzi
The same `@Preview` functions become Roborazzi screenshot tests with zero extra test code.
See `kmp-roborazzi` for the Gradle plugin wiring.
Conceptual link:
```
@Preview function → Roborazzi captures bitmap → committed as golden image
→ CI diffs on next run
```
---
## Related Skills
- `kmp-presenter-module` — the Screen/Content split that makes previews stateless
- `kmp-roborazzi` — promotes previews to CI screenshot tests
- `kmp-feature-scaffold` — the Desktop target must be declared in the feature's `:ui` convention plugin
- `kmp-unit-testing` — unit tests for the ViewModel; PDD covers the UI layer
---
## Common Anti-Patterns
- putting ViewModel logic into the `Content` composable — previews need fixed state; a ViewModel call breaks them
- one preview per file instead of `@PreviewParameterProvider` — harder to see all states at a glance
- skipping the `AppTheme {}` wrapper in previews — colors and typography look wrong; always wrap
- using Android emulator to verify layout during development — Desktop preview is 3–5× faster
- hardcoding preview data inline — use `PreviewData` object so all previews stay in sync
- shipping a `Content` composable without a matching preview stub — that leaves the
preview workflow optional and is exactly how drift sneaks in
- two preview states that render identically (e.g. a separate "Initial" preview next to
"Default" when both pass the same field values) — before adding a new named state, check
whether it's visually distinct from an existing one; a preview that duplicates another
pixel-for-pixel is noise, not coverage. One preview per genuinely distinct visual state
- a horizontally-scrolling container (`LazyRow`, carousel) in a preview with no
`Modifier.fillMaxWidth()` or explicit width on the container — an unbounded-width
scrolling row can collapse to its first item's intrinsic width or render nothing
useful in the preview panel; wrap it in a fixed-width `Box` or set `fillMaxWidth()`
so the preview reflects the real on-screen proportions
If the Desktop preview doesn't render, check that the `:ui` convention plugin declares `jvm()` and
that the Compose plugin is applied.
---
## Output Style
When asked about UI iteration, previews, or fast Compose development, respond in this order:
1. Content composable structure (state param + onIntent lambda)
2. `@PreviewParameterProvider` with all state variants
3. Desktop run command for live iteration
4. how previews promote to Roborazzi
5. PreviewData object placement
---
## Changelog
| Date | Change |
|---|---|
| 2026-07-08 | Added 2 anti-patterns from real shadcn-compose bug reports: duplicate preview states that render identically (e.g. "Initial" vs "Default"), and horizontally-scrolling containers previewed with no explicit/fillMaxWidth container width. |
| 2026-06-18 | Initial release. |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!