A Storybook‑style catalog for building & demoing Android UI in isolation. Declare a screen or component in a given state, and it shows up in a browsable catalog — no wiring, no separate apps.
Kotlin/Jetpack Compose port of the iOS Scenarios framework.
Building one screen at a time normally means booting the whole app to the right place. Scenarios flips it: each scenario is one screen/component in one state, launchable on its own — great for prototyping, design review, QA, and demos.
flowchart LR
A["@ScenarioEntry<br/>object MyScreen : Scenario"] -->|compile time| B["scenarios-ksp<br/>(KSP processor)"]
B --> C["GeneratedScenarioRegistry<br/>(auto-generated)"]
C --> D["ScenariosManager"]
D --> E["ScenariosApp()<br/>catalog + host UI"]
E -->|tap| F["Your @Composable<br/>runs in isolation"]
How discovery works: iOS scans the Objective‑C runtime. Android/DEX can't do that cheaply, so a KSP processor collects every
@ScenarioEntryat compile time and generates the registry.ScenariosManagerloads it with a singleClass.forName— no reflection, no manual registration.
git clone https://github.com/codedeman/scenarios-android
cd scenarios-android
./gradlew :sample:installDebug # install on a running emulator/deviceRequires JDK 17 and the Android SDK (compileSdk 34, minSdk 24).
Published to GitHub Packages as:
| Artifact | Coordinate |
|---|---|
| Framework (AAR) | io.github.codedeman:scenarios:0.1.0 |
| KSP processor | io.github.codedeman:scenarios-ksp:0.1.0 |
| Annotation | io.github.codedeman:scenarios-annotations:0.1.0 (comes transitively) |
Important
GitHub Packages requires auth even to read public packages. Each consumer needs a GitHub
Personal Access Token (classic) with read:packages. Put it in ~/.gradle/gradle.properties
(never commit it):
gpr.user=YOUR_GITHUB_USERNAME
gpr.key=YOUR_TOKEN1️⃣ Add the repository — settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/codedeman/scenarios-android")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}
}2️⃣ Add the dependencies — app module build.gradle.kts:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose") // Kotlin 2.0+
id("com.google.devtools.ksp") version "2.0.20-1.0.25"
}
android {
buildFeatures { compose = true }
defaultConfig { minSdk = 24 } // compileSdk >= 34
}
dependencies {
implementation("io.github.codedeman:scenarios:0.1.0")
ksp("io.github.codedeman:scenarios-ksp:0.1.0")
// @ScenarioEntry + Jetpack Compose arrive transitively from :scenarios
}Note
KSP is tied to the Kotlin version — use Kotlin 2.0.20 (matches 2.0.20-1.0.25), JDK 17, compileSdk >= 34, minSdk >= 24.
3️⃣ Declare a scenario & wire the host:
@ScenarioEntry
object MyScreenScenario : Scenario {
override val name = "My Screen"
override val kind = ScenarioKind("Screen")
override val rootViewProvider = rootView { /* any @Composable */ }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val manager = ScenariosManager(context = this)
setContent { ScenariosApp(manager) } // that's it 🎉
}
}Compose content
@ScenarioEntry
object TypographyScenario : Scenario {
override val name = "Typography"
override val kind = ScenarioKind("Design System")
override val shortDescription = "All text styles"
override val rootViewProvider = rootView {
Text("Hello", style = MaterialTheme.typography.headlineLarge)
}
}Group with categories
override val category: ScenarioCategory = "Github".asCategory()
override val subCategory: ScenarioCategory = "Views".asCategory()Host a classic Android View (interop)
@ScenarioEntry
object WebViewScenario : Scenario {
override val name = "WebView Prototype"
override val kind = ScenarioKind("Prototype")
override val rootViewProvider = androidViewProvider { context ->
WebView(context).apply { loadUrl("https://example.com") }
}
}Restrict to an audience
@ScenarioEntry
object DevOnly : Scenario, AudienceTargetable {
override val audiences = listOf(Audience.Developer)
// ...
}| 🔎 Auto‑discovery | Annotate @ScenarioEntry, done — KSP registers it |
| 🗂️ Grouping | By ScenarioKind, nested category / sub‑category |
| ⭐️ Favourites | Long‑press any scenario to pin it |
| 🔄 Reset / Refresh | Close back to catalog, or remount the current scenario |
| 🌗 Dark / Light | Per‑scenario override, persisted |
| 🧱 View interop | androidViewProvider { } embeds any classic View |
| 👥 Audiences | Show scenarios only to a target audience |
| iOS (Swift) | Android (Kotlin) |
|---|---|
Scenario protocol |
Scenario interface |
ScenarioKind |
ScenarioKind data class |
RootViewProviding → UIViewController |
RootViewProvider → @Composable |
| auto‑discovery (ObjC runtime) | KSP‑generated ScenarioRegistry |
NotificationCenter |
ScenarioEventBus (SharedFlow) |
UserDefaults |
Preferences (SharedPreferences) |
ApplicationShortcutItem |
AppShortcut (ShortcutManagerCompat) |
Audience / AudienceTargetable |
same names, Kotlin |
scenarios-android/
├── scenarios-annotations/ @ScenarioEntry marker
├── scenarios-ksp/ KSP processor → generates the registry
├── scenarios/ the framework (Compose UI + manager + interop)
└── sample/ demo app with example scenarios
# local smoke test → ~/.m2/repository/io/github/codedeman/
./gradlew publishToMavenLocal
# publish to GitHub Packages (needs gpr.user / gpr.key with write:packages)
./gradlew publishAllPublicationsToGitHubPackagesRepositoryOr just create a GitHub Release → the publish.yml workflow does it with the built‑in GITHUB_TOKEN. Bump version in the root build.gradle.kts (subprojects { version = ... }) per release. Only :scenarios, :scenarios-ksp, :scenarios-annotations are published — :sample is not.
| Symptom | Fix |
|---|---|
Inconsistent JVM-target compatibility (compileJava 17 / compileKotlin 21) |
Android Studio runs Gradle on JDK 21; JVM modules already pin jvmTarget = 17. Sync the project. |
CI: Cannot find a Java installation ... matching Java 21, vendor 'jetbrains' |
Delete gradle/gradle-daemon-jvm.properties (it's git‑ignored). It pins the daemon to the Android Studio JBR, unavailable on Linux runners. |
Consumer: Could not resolve io.github.codedeman:scenarios |
Add the GitHub Packages repo and a read:packages PAT in ~/.gradle/gradle.properties. |
| Catalog is empty | Scenarios must be annotated @ScenarioEntry and declared in the module where KSP runs. |
FeatureScenario / FeatureContext (multi‑configuration flows), the iPad split‑view layout, and separate Internal/Production targets (would map to Android build variants).
MIT


