Preview Screenshot

HotSwan includes a captureAllPreviews Gradle task that automatically finds every @Preview function in your project, launches each one on a real device, captures a screenshot, and generates a browsable HTML catalog. No test code to write. Just run the task and share the result.

Preview Screenshot is Android only. HotSwan registers the task on modules that apply the Android application plugin, so it captures on a device or emulator, and a module that only targets Compose Desktop or iOS has no such task to run. Hot reload itself runs on all three.

Screenshot testing requires the Preview Runner setup. If you have not configured it yet, follow the Preview Runner Setup guide first.

Capture All Previews

./gradlew captureAllPreviews

The task scans all .kt files under src/, detects @Preview annotations, and launches each composable on the connected device one by one. After all captures complete, an index.html file is generated in the default output directory: .hotswan/preview-captures/

Explore Live Preview Catalog (Pokedex Compose)→

Before You Run

The task drives a device you already have in front of you. It checks two things first and stops with a message instead of capturing a wrong catalog.

The app has to be running

Captures happen inside your running app's process, which is what makes DI, network, and database work in a preview. Install and launch the app first. If it is not running, the task stops with App is not running on device. Please run the app first. and nothing is captured.

One device, or name the one you want

With a single device attached there is nothing to configure. With more than one and no ANDROID_SERIAL set, the task refuses rather than guessing: it force launches a preview Activity, turns System UI Demo Mode on, and screenshots whichever device it picked, so a wrong guess produces a confident catalog of the wrong screen. Set ANDROID_SERIAL to a serial from adb devices, or detach the others.

# Phone and emulator both attached: name the one to capture
ANDROID_SERIAL=emulator-5554 ./gradlew captureAllPreviews

HTML Catalog Features

The generated HTML catalog is a self-contained file you can open in any browser, share with your team, or host as documentation.

Dark / Light theme

Toggle between dark and light mode. Dark mode is the default.

Search

Filter previews by name or module with instant search.

Module grouping

Previews are grouped by Gradle module. Toggle grouping on or off with a single click.

Fullscreen modal

Click any preview screenshot to view it at full resolution in an overlay modal.

Device & timestamp info

Each report includes the device model, app package name, and generation timestamp.

Gradle Configuration

HotSwan provides a preview configuration block inside hotSwanCompiler for customizing the preview capture behavior:

hotSwanCompiler {
    preview {
        // Output directory for captured previews (relative to project root)
        outputDir.set(".hotswan/preview-captures")

        // Delay between launching a preview and capturing its screenshot
        renderDelayMs.set(2500L)

        // Enable SDK documentation mode for rich API docs
        sdkModeEnabled.set(false)

        // System UI Demo Mode during capture, for a deterministic status bar
        demoMode.set(true)
    }
}

outputDir

Directory where preview screenshots and the HTML catalog are saved. Relative to the project root. Default: .hotswan/preview-captures.

renderDelayMs

Milliseconds to wait after launching a preview before taking the screenshot. Increase this for composables that load data asynchronously. Default: 2500.

sdkModeEnabled

When enabled, the HTML catalog includes KDoc descriptions and parameter tables extracted from the composable that each preview wraps. Ideal for design system libraries. Default: false.

demoMode

Puts the device into System UI Demo Mode for the length of the run, so every screenshot has the same status bar: clock at 12:00, battery full and unplugged, full signal, notifications hidden. Without it the clock and battery differ between runs and every image compares as changed. Default: true, so this is on unless you turn it off.

It is worth knowing what this leaves behind. Enabling it runs adb shell settings put global sysui_demo_allowed 1 on the device and then broadcasts the demo commands. When the run finishes, HotSwan broadcasts command exit, which restores the normal status bar, but sysui_demo_allowed stays set to 1. That global setting persists across runs and reboots until you clear it yourself:

adb shell settings put global sysui_demo_allowed 0

On a shared or a physical device you care about, set demoMode.set(false) and accept a live status bar in the catalog.

Per-Preview Render Delay

Some previews need more time than others. A simple static layout is ready in under a second, but a composable that loads a network image may need several seconds. Instead of raising the global delay for every preview, you can use @PreviewScreenshot to set a delay per preview.

@Preview
@PreviewScreenshot(renderDelay = 5000)
@Composable
fun NetworkImagePreview() {
    AsyncImage(
        model = "https://example.com/photo.jpg",
        contentDescription = null,
    )
}

Previews without @PreviewScreenshot use the global renderDelayMs value. Previews with the annotation use the specified delay instead. This keeps fast previews fast while giving slow ones the time they need.

@PreviewScreenshot

renderDelay — Milliseconds to wait before capturing. Overrides the global renderDelayMs.
tags — Group labels for selective capture with -Photswan.preview.tags. See Selective Capture with Tags below.

Selective Capture with Tags

As a catalog grows, capturing every preview on every run gets slow. Tags let you group previews into named sets and capture only the sets you want. Add one or more labels to @PreviewScreenshot:

@Preview
@PreviewScreenshot(tags = ["smoke", "detail"])
@Composable
fun PokemonDetailPreview() {
    PokemonDetail(pokemon = samplePokemon)
}

Then pass -Photswan.preview.tags when you run the task to capture only previews whose tags include any of the requested labels:

# Capture only previews tagged "smoke"
./gradlew captureAllPreviews -Photswan.preview.tags=smoke

# Capture previews tagged "smoke" OR "detail" (comma separated)
./gradlew captureAllPreviews -Photswan.preview.tags=smoke,detail

# No filter: capture every preview, as before
./gradlew captureAllPreviews

Matching is an OR across the requested labels: a preview is captured when any of its tags is requested, and previews with no matching tag are skipped. Without the -Photswan.preview.tags flag every preview is captured, exactly as before, so existing setups are unaffected.

  • ✓Capture a fast smoke set on every pull request and the full catalog nightly
  • ✓Capture only the screens for the feature you are working on
  • ✓Split a large catalog into per-team or per-flow groups that each render on their own schedule

Shared tags with a custom multipreview annotation

Tags can also be placed on a custom multipreview annotation (an annotation that is itself meta-annotated with @Preview). Every function that uses it inherits the tags, so you can bake a capture group into one reusable annotation:

@Preview
@PreviewScreenshot(tags = ["smoke"])
annotation class SmokePreview

@SmokePreview            // inherits tags = ["smoke"]
@Composable
fun HomePreview() {
    Home()
}

SDK Documentation Mode

For design system and library/SDK developers, enable sdkModeEnabled to generate rich API documentation alongside your preview screenshots. HotSwan traces each @Preview function to find the composable it wraps, then extracts the KDoc description, parameter names, types, and default values.

hotSwanCompiler {
    preview {
        sdkModeEnabled.set(true)
        renderDelayMs.set(3000L)
    }
}

With SDK mode enabled, each card in the HTML catalog shows:

  • ✓Composable name chip: the actual composable being previewed (not the preview wrapper function)
  • ✓KDoc description: extracted from the composable's documentation comment
  • ✓Parameter table: name, type, default value, and @param descriptions
  • ✓Collapsible parameters: composables with more than 5 parameters show an expand/collapse toggle

This replaces the need for separate tools like Showkase or manual component documentation. Run captureAllPreviews in CI to automatically generate and publish your component catalog on every pull request.

When a Preview Fails to Launch

If any preview could not be launched on the device, captureAllPreviews ends the build with a failure. A catalog of screens nobody launched looks complete and is not, so the task refuses to report success on one.

One bad preview does not abandon the sweep. The failure is recorded, the run continues to the next preview, the HTML catalog is written with everything that did capture, and only then does the task fail. You keep the artifacts and you also get a red build, with every failure named:

captureAllPreviews: 2 of 34 preview(s) could not be launched, so they were NOT captured:
  - HomeScreenPreview: the app does not contain HotSwanPreviewActivity ...
  - DetailScreenPreview: am start failed with exit code 1 and printed nothing

The most common cause is a missing preview dependency. HotSwan does not add :preview for you, so an app that never added it has no HotSwanPreviewActivity to launch and every preview fails the same way. The Preview Runner Setup guide has the one line to add. After adding it, reinstall the app: the task launches against the build already on the device, not the one in your source tree.

CI Integration

Because captureAllPreviews is a standard Gradle task that uses ADB, it works in any CI environment that provides an Android emulator. GitHub Actions with reactivecircus/android-emulator-runner is the simplest setup:

- name: Run emulator and capture screenshots
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 31
    arch: x86_64
    profile: pixel_6
    script: |
      adb install -r app/build/outputs/apk/debug/app-debug.apk
      adb shell am start -n com.your.app/.MainActivity
      sleep 10
      ./gradlew :app:captureAllPreviews

The captured screenshots can be uploaded as build artifacts or deployed directly to GitHub Pages to create a living component catalog that updates on every push.

Treat the task as a gate and not only as an artifact step. As described in When a Preview Fails to Launch, a preview that never launched fails the build, so a pipeline that only ever uploaded images can start going red. That is the intent: it is how you find out a preview stopped working. Upload the catalog with if: always() so a failed run still hands you the images it did capture, and keep the adb install and am start steps above, because the task needs the app running before it starts.

System UI Demo Mode is on by default and leaves the sysui_demo_allowed global setting behind on the device. On a throwaway CI emulator that does not matter. On a physical device wired into a self hosted runner it persists, so clear it or set demoMode.set(false).

Comparison with other tools

Tools like Paparazzi and Roborazzi take a different approach to composable screenshot capture. They render composables on the host JVM using a layout rendering engine, which means they run without a device or emulator. This makes them well suited for fast regression testing in CI.

HotSwan captures screenshots on a real device or emulator. This gives you the actual rendering your users will see, including system UI, font rendering, and device specific behavior.

HotSwanPaparazzi / Roborazzi
RenderingReal device or emulatorHost JVM approximation
System UIIncluded (status bar, navigation bar)Not included
Runtime behaviorFull (image loading, network, database)UI shell only
Test code requiredNone. Scans @Preview automaticallyTest class per composable
OutputImage files + searchable HTML catalog with module groupingImage files
SDK documentationKDoc + parameter table extractionNot available
CI integrationEmulator required (GitHub Actions supported)No emulator needed

The two approaches are complementary. Paparazzi and Roborazzi are focused on regression testing with golden image comparison. HotSwan is focused on visual cataloging and documentation, with screenshots that reflect actual device behavior. You can use both in the same project for different purposes.