A Kotlin Multiplatform debug panel for Compose apps.
Network inspector, log viewer, typed preferences, and custom screens — in one composable your app renders however and whenever you want during development.
Documentation · Live demo · Installation · Quick start
- 🌐 Inspect HTTP traffic without a proxy — built-in Ktor integration; any other client plugs in via a low-level store API.
- 🪵 View logs without ADB — Kermit bridge ships out of the box; Timber and friends drop in via a 4-line
LogCollector. - 🎚️ Flip feature flags from the panel — annotate a class, KSP generates the DataStore wiring and a ready-to-use UI.
- 🧩 Wrap any Composable as a debug screen — internal QA dashboards, environment switchers, build-info pages.
- ⚡ Zero release-build cost —
core:noopswaps the panel for a passthrough composable, andnetwork-monitor:noop/log-monitor:noopstrip the recording side too; release binaries don't ship one byte of Sidekick UI or database code. - 🖼️ Compose Multiplatform — one codebase, five targets: Android, iOS, Desktop (JVM), Web (JS), Web (Wasm).
- 🎨 Theme-aware — applies its own light/dark palette by default, or inherits your
MaterialThemewith one flag.
| Plugin | What it does | Platforms |
|---|---|---|
| Network Monitor | Captures every HTTP request / response. Ktor built-in; OkHttp and others via NetworkMonitorStore. |
|
| Log Monitor | Color-coded log feed with level chips and search. Kermit bridge built-in. | |
| Preferences | Typed settings UI generated from @Preference annotations via KSP. |
|
| Custom Screens | Wrap any Composable as a debug card. Full DI access. | |
| Your plugin | Implement SidekickPlugin — full module, your own DI scope, anything goes. |
depends on what you publish |
¹ Wasm uses in-memory preferences (DataStore has no Wasm driver) — values do not persist across reloads.
Each Sidekick BOM is built against a fixed Kotlin and Compose Multiplatform stack. Consumers need to match the Kotlin and Compose Multiplatform minor versions (klib ABI is not stable across minors) and meet the compileSdk constraint.
| BOM | Kotlin | Compose Multiplatform | Android compileSdk |
|---|---|---|---|
2026.05.26 |
2.3.21 | 1.11.0 | 37+ ¹ |
2026.05.18 |
2.3.20 | 1.10.3 | 36+ |
2026.05.16 |
2.3.20 | 1.10.3 | 36+ |
Android minSdk 24+ across all releases.
¹ 2026.05.26 transitively depends on androidx.compose.material3.adaptive:*:1.3.0-beta01, which enforces compileSdk ≥ 37 at the AAR-metadata level. Older BOMs used the CMP-namespaced org.jetbrains.compose.material3.adaptive:adaptive:1.2.0, which has no such constraint.
Pin the BOM once; every dependency line is then version-agnostic. The BOM is calendar-versioned (YYYY.MM.DD) — check the Maven Central badge above for the latest.
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
// BOM pins every Sidekick artifact. Constraints propagate to all
// configurations that extend `implementation` — including the
// Android `debugImplementation` / `releaseImplementation` below.
implementation(platform("dev.parez.sidekick:bom:2026.05.17"))
// `compileOnly` here gives commonMain the type stubs without
// putting the real plugin jars on Android release's runtime
// classpath — they would collide with the noop variants.
compileOnly("dev.parez.sidekick:network-monitor-ui")
compileOnly("dev.parez.sidekick:network-monitor-ktor")
compileOnly("dev.parez.sidekick:log-monitor-ui")
compileOnly("dev.parez.sidekick:log-monitor-kermit")
implementation("dev.parez.sidekick:preferences")
implementation("dev.parez.sidekick:custom-screen")
}
}
}
dependencies {
// Android only. `debugImplementation` / `releaseImplementation` are AGP
// configurations and don't exist on JVM / iOS / JS / WasmJS — see the
// per-platform notes below.
debugImplementation("dev.parez.sidekick:shell")
releaseImplementation("dev.parez.sidekick:noop")
// Recording plugins follow the same swap: debug gets the real api+ui
// +ktor/kermit trio; release gets the noop, which strips SQLDelight and
// makes every recordX/install hook a no-op.
debugImplementation("dev.parez.sidekick:network-monitor-ui")
debugImplementation("dev.parez.sidekick:network-monitor-ktor")
releaseImplementation("dev.parez.sidekick:network-monitor-noop")
debugImplementation("dev.parez.sidekick:log-monitor-ui")
debugImplementation("dev.parez.sidekick:log-monitor-kermit")
releaseImplementation("dev.parez.sidekick:log-monitor-noop")
}Using a Gradle version catalog?
Drop this in gradle/libs.versions.toml. One version key covers every artifact — the BOM provides versions for libraries; only the Gradle plugin needs its own (Gradle's plugin DSL doesn't honor BOMs).
[versions]
sidekick = "2026.05.17" # BOM version (YYYY.MM.DD) — bump to track the latest release
[libraries]
sidekick-bom = { module = "dev.parez.sidekick:bom", version.ref = "sidekick" }
# Everything below is BOM-managed — no version needed.
sidekick-shell = { module = "dev.parez.sidekick:shell" }
sidekick-noop = { module = "dev.parez.sidekick:noop" }
sidekick-network-monitor-ui = { module = "dev.parez.sidekick:network-monitor-ui" }
sidekick-network-monitor-ktor = { module = "dev.parez.sidekick:network-monitor-ktor" }
sidekick-network-monitor-noop = { module = "dev.parez.sidekick:network-monitor-noop" }
sidekick-log-monitor-ui = { module = "dev.parez.sidekick:log-monitor-ui" }
sidekick-log-monitor-kermit = { module = "dev.parez.sidekick:log-monitor-kermit" }
sidekick-log-monitor-noop = { module = "dev.parez.sidekick:log-monitor-noop" }
sidekick-preferences = { module = "dev.parez.sidekick:preferences" }
sidekick-custom-screen = { module = "dev.parez.sidekick:custom-screen" }
[plugins]
# Plugin markers are published at the BOM's calendar version too, so you
# pin the same `sidekick` key here. The marker resolves transparently to
# the impl jar at its current preferences-family version — you don't see
# that number.
sidekick-preferences = { id = "dev.parez.sidekick.preferences", version.ref = "sidekick" }Then in build.gradle.kts: implementation(platform(libs.sidekick.bom)), implementation(libs.sidekick.network.monitor.plugin), debugImplementation(libs.sidekick.shell), alias(libs.plugins.sidekick.preferences), etc.
Per-platform notes & KSP setup
-
Non-Android targets (Desktop / iOS / JS / Wasm) —
debugImplementationandreleaseImplementationare AGP configurations and only work on Android. For other targets the consumer picks the real or noop module manually in each leaf source set (jvmMain,iosMain,jsMain,wasmJsMain). A property-gated recipe (run prod builds with-Psidekick.noop=true):val sidekickNoop = (findProperty("sidekick.noop") as? String).toBoolean() jvmMain.dependencies { if (sidekickNoop) { implementation("dev.parez.sidekick:noop") implementation("dev.parez.sidekick:network-monitor-noop") implementation("dev.parez.sidekick:log-monitor-noop") } else { implementation("dev.parez.sidekick:shell") implementation("dev.parez.sidekick:network-monitor-ui") implementation("dev.parez.sidekick:network-monitor-ktor") implementation("dev.parez.sidekick:log-monitor-ui") implementation("dev.parez.sidekick:log-monitor-kermit") } }
Mirror the same shape in
iosMain.dependencies,jsMain.dependencies, andwasmJsMain.dependencies. -
KSP for Preferences — easiest path: apply the
dev.parez.sidekick.preferencesGradle plugin, which wires the KSP processor, the generated-sources directory, and the task dependencies for you. Full snippet (and a manual alternative) in docs/installation.md. -
Android
ContentProvider—dev.parez.sidekick:plugin-apiships aSidekickInitializerthat auto-initializes the library context. No manual call required.
The client app owns the FAB and visibility; Sidekick only renders the panel. The trailing actions slot is where you place the close button.
@Composable
fun App() {
val networkPlugin = remember { NetworkMonitorPlugin() }
val logPlugin = remember { LogMonitorPlugin() }
var sidekickVisible by remember { mutableStateOf(false) }
MaterialTheme {
Box(Modifier.fillMaxSize()) {
MyAppContent()
SmallFloatingActionButton(
onClick = { sidekickVisible = true },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
) {
Icon(Icons.Default.BugReport, contentDescription = "Open Sidekick")
}
AnimatedVisibility(
visible = sidekickVisible,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
) {
Sidekick(
plugins = listOf(networkPlugin, logPlugin),
actions = {
IconButton(onClick = { sidekickVisible = false }) {
Icon(Icons.Default.Close, contentDescription = "Close")
}
},
)
}
}
}
}Use any trigger — a shake gesture, a hidden tap zone, a build-type check. Sidekick is just a composable.
| Topic | |
|---|---|
| Installation | Per-platform notes, KSP setup. |
| Quick start | Wire-up snippet, header customization, appInfo. |
| Release builds | Swap core:shell → core:noop and the monitor families to their noop variants. Zero overhead. |
| Theming | Use Sidekick's theme or inherit yours. HTTP badge colors. |
| Network Monitor | Ktor integration, OkHttp recipe, sanitization, retention. |
| Log Monitor | Kermit bridge, Timber recipe, custom LogCollector. |
| Preferences | @Preference annotations, KSP setup, DataStore migration. |
| Custom Screens | Wrap any Composable as a debug card. |
| Creating a Custom Plugin | Implement SidekickPlugin end-to-end. |
Contributions welcome. The project layout, build conventions, and the architecture decisions behind the plugin system are documented in CLAUDE.md. Run all tests with:
./gradlew allTestsPublish snapshots locally with:
./gradlew publishToMavenLocal --no-configuration-cacheApache 2.0 — see LICENSE.
The demo app uses data from PokéAPI — a free, open RESTful Pokémon API. See pokeapi.co/about for license and usage details.