
Debug overlay embedding a floating panel to inspect HTTP traffic, view logs, toggle feature flags, manage typed preferences via codegen, and add custom debug screens—zero release overhead.
A Kotlin Multiplatform debug overlay SDK.
Network inspector, log viewer, typed preferences, and custom screens — in one floating panel your app embeds during development.
Documentation · Live demo · Installation · Quick start
LogCollector.core:noop swaps the overlay for a passthrough composable, and network-monitor:noop / log-monitor:noop strip the recording side too; release binaries don't ship one byte of Sidekick UI or database code.MaterialTheme with 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.
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")
}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.
Non-Android targets (Desktop / iOS / JS / Wasm) — debugImplementation and releaseImplementation are 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, and wasmJsMain.dependencies.
KSP for Preferences — easiest path: apply the dev.parez.sidekick.preferences Gradle 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-api ships a SidekickInitializer that 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.
A Kotlin Multiplatform debug overlay SDK.
Network inspector, log viewer, typed preferences, and custom screens — in one floating panel your app embeds during development.
Documentation · Live demo · Installation · Quick start
LogCollector.core:noop swaps the overlay for a passthrough composable, and network-monitor:noop / log-monitor:noop strip the recording side too; release binaries don't ship one byte of Sidekick UI or database code.MaterialTheme with 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.
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")
}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.
Non-Android targets (Desktop / iOS / JS / Wasm) — debugImplementation and releaseImplementation are 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, and wasmJsMain.dependencies.
KSP for Preferences — easiest path: apply the dev.parez.sidekick.preferences Gradle 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-api ships a SidekickInitializer that 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.