Documentation
Start here
Getting started
Install Composium from Maven Central, configure KSP, and build your first Jetpack Compose scene catalog.
Requirements
- Android
minSdk 24 - Consumer project
compileSdk 36 - JVM target
11 - Kotlin
2.3.21or newer mavenCentral()andgoogle()in your consumer project
If you use KSP, choose a KSP2 plugin version compatible with your Kotlin and AGP versions.
The baseline configuration uses Kotlin 2.3.21, KSP 2.3.9, AGP 8.10.1, and Gradle 8.11.1.
Installation
Set your app or host module to compileSdk 36 before adding Composium.
Add repositories:
repositories {
google()
mavenCentral()
}
Recommended: with KSP
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("io.github.oleginvoke:composium:1.3.0-alpha03")
ksp("io.github.oleginvoke:composium-processor:1.3.0-alpha03")
}
Use this mode when you want automatic scene collection.
In a multi-module project, keep all scene declarations and the Composium KSP processor in a single showcase module, such as :sample or :catalog. That module depends on the modules containing the UI components and declares scenes for those components. It can run as a standalone sample app or expose a public composable entry point for a host app to display the showcase.
Automatic discovery collects scenes declared in that showcase module; it does not collect annotated scenes from compiled dependencies. Do not configure the Composium processor in multiple scene-containing modules included in the same app: each generates the same registry class, causing a duplicate-class build error. The runtime dependency itself can be used in multiple modules; this restriction applies to scene discovery and registry generation.
Optional: without KSP
dependencies {
implementation("io.github.oleginvoke:composium:1.3.0-alpha03")
}
Use this mode when you do not want to add KSP to the consumer project. In this case:
- do not add the processor;
- do not use
@ComposiumSceneor@ComposiumSceneCatalog; - register scenes manually through
Composium.registerAll(...).
Quick Start With KSP
KSP is the primary integration path.
There are two discovery styles:
- annotate individual scene properties with
@ComposiumScene; - or annotate an object with
@ComposiumSceneCatalog.
You do not need to use both at the same time. Pick the style that matches how you want to organize scenes in your project.
Option 1: annotate individual scene properties
Use this style when you want flat, explicit scene declarations and prefer to mark each scene directly.
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import oleginvoke.com.composium.ComposiumScene
import oleginvoke.com.composium.ComposiumScreen
import oleginvoke.com.composium.scene
enum class ButtonVariant {
Primary,
Secondary,
Danger,
}
sealed interface ButtonSize {
object Small : ButtonSize
object Medium : ButtonSize
object Large : ButtonSize
}
@ComposiumScene
internal val primaryButton by scene(
group = "Buttons/Primary",
name = "Filled",
) { contentPadding ->
val enabled: Boolean by param(true)
val text: String by param("Continue")
val variant: ButtonVariant by param(ButtonVariant.Primary)
val size: ButtonSize by param(ButtonSize.Medium)
Box(Modifier.padding(contentPadding)) {
AppButton(
text = text,
enabled = enabled,
variant = variant,
size = size,
)
}
}
@Composable
fun DebugCatalog() {
ComposiumScreen()
}
Option 2: annotate a scene catalog object
Use this style when you want to keep several related scenes together inside one object and let KSP collect all non-private Scene properties from it.
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import oleginvoke.com.composium.ComposiumSceneCatalog
import oleginvoke.com.composium.ComposiumScreen
import oleginvoke.com.composium.scene
@ComposiumSceneCatalog
internal object FormScenes {
val loginDefault by scene(group = "Forms/Auth", name = "Login / default") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
LoginForm()
}
}
val loginLoading by scene(group = "Forms/Auth", name = "Login / loading") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
LoginForm(isLoading = true)
}
}
}
@Composable
fun DebugCatalog() {
ComposiumScreen()
}
What KSP collects:
- every property annotated with
@ComposiumScene; - every non-private
Sceneproperty declared inside an object annotated with@ComposiumSceneCatalog.
These are two independent discovery styles, not a required pair of annotations on the same scene setup.
Quick Start Without KSP
Manual mode uses the same scene {} API, but skips both KSP and annotations.
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import oleginvoke.com.composium.Composium
import oleginvoke.com.composium.ComposiumScreen
import oleginvoke.com.composium.scene
internal val primaryButton by scene(group = "Buttons/Primary", name = "Filled") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
AppButton(text = "Continue")
}
}
internal object FormScenes {
val loginDefault by scene(group = "Forms/Auth", name = "Login / default") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
LoginForm()
}
}
}
@Composable
fun DebugCatalog() {
LaunchedEffect(Unit) {
Composium.registerAll(
primaryButton,
FormScenes.loginDefault,
)
}
ComposiumScreen()
}
Notes:
- manual registration identifies scenes by their group and name;
- scenes still use the same runtime API as KSP mode;
- annotations are not needed in manual mode.
For both automatic and manual registration, scene names must be unique within a group. Registering the same Scene instance again is safe. If a different scene uses the same group and name, the first scene is kept and Composium logs a warning with the Composium tag.
Repeated reads of a property declared with val MyScene by scene { ... } return the same Scene instance. Reading the property does not render its content.
When constructing Scene(...) directly, keep and reuse the instance for repeated registration.