Skip to content

Repository files navigation

🎬 Scenarios for Android

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.

Release Kotlin Compose minSdk License Publish

Kotlin/Jetpack Compose port of the iOS Scenarios framework.


📸 What it looks like

Catalog (grouped by kind) Compose ⇄ View interop Dark mode toggle
Auto‑discovered scenarios, nested categories, ⭐️ favourites A native WebView hosted inside the Compose host Per‑scenario light/dark, remembered across navigation

⚡ Why

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"]
Loading

How discovery works: iOS scans the Objective‑C runtime. Android/DEX can't do that cheaply, so a KSP processor collects every @ScenarioEntry at compile time and generates the registry. ScenariosManager loads it with a single Class.forNameno reflection, no manual registration.


🚀 Quick start (try the sample)

git clone https://github.com/codedeman/scenarios-android
cd scenarios-android
./gradlew :sample:installDebug     # install on a running emulator/device

Requires JDK 17 and the Android SDK (compileSdk 34, minSdk 24).


📦 Use it in your own app

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_TOKEN

1️⃣ Add the repositorysettings.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 🎉
    }
}

✍️ Writing scenarios

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)
    // ...
}

🧩 Features

🔎 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

🔁 Concept mapping (iOS → Android)

iOS (Swift) Android (Kotlin)
Scenario protocol Scenario interface
ScenarioKind ScenarioKind data class
RootViewProvidingUIViewController RootViewProvider@Composable
auto‑discovery (ObjC runtime) KSP‑generated ScenarioRegistry
NotificationCenter ScenarioEventBus (SharedFlow)
UserDefaults Preferences (SharedPreferences)
ApplicationShortcutItem AppShortcut (ShortcutManagerCompat)
Audience / AudienceTargetable same names, Kotlin

🏗️ Module structure

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

🛠️ Publishing (maintainers)

# local smoke test → ~/.m2/repository/io/github/codedeman/
./gradlew publishToMavenLocal

# publish to GitHub Packages (needs gpr.user / gpr.key with write:packages)
./gradlew publishAllPublicationsToGitHubPackagesRepository

Or 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.


🩹 Troubleshooting

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.

🗺️ Not yet ported

FeatureScenario / FeatureContext (multi‑configuration flows), the iPad split‑view layout, and separate Internal/Production targets (would map to Android build variants).

📄 License

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages