collectAllModulePreviews

Provides a delegate that collects @Preview functions from this module and every dependency module that has the Compose Preview Lab Gradle plugin applied.

The compiler plugin replaces the body at compile time so that the returned PreviewExport holds a concatenation of this module's previews and the previews of every dependency module on the classpath. Discovery is fully automatic — applying the Gradle plugin in a dependency module is enough; collectModulePreviews() does not need to be declared there. Modules that do declare it explicitly continue to work unchanged.

Cross-module aggregation by target / Kotlin version:

  • JVM / Android: works from Kotlin 2.3.20 (the FIR per-declaration hint generator stabilised in 2.3.20; JVM bytecode dependency resolution has no incremental-compile complications).

  • JS / Wasm JS / iOS / Native: works from Kotlin 2.3.21 (additionally requires the KT-82395 KLIB IC fix that landed in 2.3.21).

The compiler plugin emits a per-@Preview hint (previewHint_<scope>(value: PreviewHintMarker_<hash>?): CollectedPreview in the me.tbsten.compose.preview.lab.hints package) and the consumer side discovers them via IrPluginContext.referenceFunctions, which works equally for JVM bytecode and KLIB modules.

On older Kotlin the plugin's IR pass detects the val x by collectAllModulePreviews() delegated-property pattern and reports a compile-time error with a clear upgrade-or-downgrade message. Direct calls outside a property delegate (which are not the supported usage) compile and fall back to the IllegalStateException thrown by the placeholder body. collectModulePreviews() (single-module) continues to work on every Kotlin version.

Mixed-classpath caveat: the aggregation only sees dependency-module previews if the dependency artifact itself was compiled with the Compose Preview Lab plugin under Kotlin 2.3.20+ (so its bytecode / KLIB carries the synthetic previewHint_<scope> overloads we walk via referenceFunctions). A dependency built with an older compiler — or built without the plugin — emits no marker / hint pair and is silently absent from the aggregated result. Compose Preview Lab cannot detect this at compile time; if a known-existing dep @Preview does not show up downstream, check the dep's Kotlin version + plugin application first.

Example — app module aggregating its own previews and any library previews on the classpath:

// app/src/commonMain/kotlin/Previews.kt
val appPreviews by collectAllModulePreviews()

Example — dependency module just applies the Gradle plugin; no Previews.kt file required:

// uiLib/build.gradle.kts
plugins {
id("me.tbsten.compose.preview.lab")
}
// any @Preview function in uiLib is now picked up by upstream collectAllModulePreviews()

This stable no-arg overload delegates to the experimental scope-aware overload with the default-scope sentinel, so consumers do not have to opt in unless they need explicit scope filtering. See the scope-aware overload below for the full scope semantics.

Return

a PreviewExport delegate wrapping the aggregated preview list; the body is replaced by the compiler plugin

See also


Scope-aware variant of collectAllModulePreviews that limits the aggregated result to previews whose @ComposePreviewLabOption(collectScopes = [...]) array contains scope.

Experimental: the scope feature is still stabilizing. Consumers must opt in with @OptIn(ExperimentalComposePreviewLabApi::class) (or @file:OptIn(...)) to call this overload. The no-arg collectAllModulePreviews overload remains stable for callers that do not need explicit scope filtering.

Example:

@file:OptIn(me.tbsten.compose.preview.lab.ExperimentalComposePreviewLabApi::class)

val designPreviews by collectAllModulePreviews(scope = "design")

Return

a PreviewExport delegate wrapping the aggregated preview list; the body is replaced by the compiler plugin

Parameters

scope

The collection scope to draw from. Only previews annotated with the matching @ComposePreviewLabOption(collectScopes = ["..."]) end up in the result. The literal value "default" (= ComposePreviewLabOption.DefaultCollectScope) acts as a sentinel: it does not mean a literal "default" bucket — the compiler plugin substitutes it with the calling module's composePreviewLab.collectPreviews.defaultCollectScope Gradle DSL value (which itself defaults to "default"). The substitution is strictly per-module: a library that pins its own previews to defaultCollectScope = "acme_ui" will register them under acme_ui, but a downstream consumer app's collectAllModulePreviews() (= the sentinel, substituted with the consumer's DSL value) will NOT see them unless the consumer explicitly asks collectAllModulePreviews(scope = "acme_ui"). This isolation is the primary reason the DSL exists.

The argument must reach the compiler plugin's IR pass as a compile-time string constant — an inline string literal or a const val reference both work, because both end up as an IrConst<String> before our pass runs. Non-const vals, string concatenations, and other expressions that produce an IrCall / IrStringConcatenation are reported as compile errors. The resolved value must also match [A-Za-z0-9_]+ because the plugin embeds it into the synthetic hint function name.

See also