All posts

KMP Inspector: An In-App Debugger for Compose Multiplatform on Android, iOS and Desktop

By Muhammad Waqas · · 7 min read

KMP Inspector on a desktop window showing captured network requests with status codes, timings and sizes

Debugging a Kotlin Multiplatform app often means debugging it three different ways. On Android you have Android Studio's inspectors, as long as a cable is plugged in. On iOS you are reading the Xcode console. On desktop you are adding println calls. And when a tester reports a bug from their own phone, none of those tools are there.

I built KMP Inspector to put one set of debugging tools inside the app itself. It is an open-source library for Compose Multiplatform: a small bubble floats over your app, and tapping it opens a full inspector for network requests, the database, logs and crashes, on Android, iOS and desktop, from a single codebase.

This post covers what it shows, how to add it, and how to keep it out of your release builds. There are also detailed guides on inspecting Ktor network requests and inspecting a Room database.

Why an in-app debugger for Kotlin Multiplatform

  • Platform tools stay on their platform. Android Studio's Network and Database Inspectors only work for Android, and only while the device is connected to your computer.
  • Testers are not at your desk. When someone reports a bug on a physical phone, you want the failing request or the crash trace from that phone, not a request to reproduce it later.
  • Shared code deserves shared tools. If your networking, database and business logic live in common code, it helps to inspect them the same way on every target.

KMP Inspector does not replace your IDE's debugger. It covers the gap between "it works on my machine" and "here is exactly what the app did on that device".

What KMP Inspector shows

The inspector has five panels:

  • Network: every request with its method, URL, status code, duration and payload sizes, plus full headers and bodies, with quick filters for errors, slow requests and writes.
  • Database: your tables and their rows, a SQL editor, and cell editing when a live database is attached.
  • Background Work: on Android, a mirror of WorkManager's job list.
  • Logs: a 2,000-line log buffer you can filter by level and tag, and pause while you read.
  • Crashes: fatal crashes kept across app restarts, plus non-fatal exceptions you record yourself.
KMP Inspector desktop layout with the request list on the left and a detail area on the right
On a wide window the Network panel shows the list and the request details side by side.

How it works

Everything starts with a bubble. You wrap your app's root composable once, and a draggable bubble floats over every screen. It shows how many new events arrived and turns red when a crash is captured. Tapping it opens the inspector full screen, and closing it takes you back to exactly where you were.

How data gets in depends on the platform. On Android, one call in your Application collects crashes, logcat and WorkManager jobs automatically, and one line each adds OkHttp and Room. On iOS and desktop the library draws the UI, and two companion modules fill it: one for the Ktor HTTP client and one for Room. Anything else can be reported through a small API, so it works with any HTTP client, database or scheduler you already use.

Add it to your project

KMP Inspector is published on Maven Central. On Android, use the real library in debug builds and the no-op version in release builds. The no-op has the same classes and functions with empty bodies, so every call site still compiles:

build.gradle.kts
dependencies {
    debugImplementation("io.github.waqas028:kmp-inspector:1.0.0")
    releaseImplementation("io.github.waqas028:kmp-inspector-no-op:1.0.0")
}

If you use Ktor or Room, add the companion modules too. All four artifacts are at version 1.0.0:

build.gradle.kts
implementation("io.github.waqas028:kmp-inspector-ktor:1.0.0")
implementation("io.github.waqas028:kmp-inspector-room:1.0.0")

Android: one line in your Application

Kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        KmpInspector.install(this)
    }
}

That single call floats the bubble over every Activity, whether it uses XML, Fragments, Jetpack Compose or a mix. It captures fatal crashes across restarts, streams the app's own logcat into the Logs panel, and mirrors WorkManager jobs when WorkManager is on the classpath. It does nothing unless the build is debuggable. Network and database traffic need one line each:

Kotlin
// Network: add after your header interceptors, so the recording matches what was sent
OkHttpClient.Builder()
    .addInterceptor(KmpInspectorInterceptor())

// Database: Room 2.7 or newer
KmpInspector.attach(database, fileName = "app.db")

iOS and desktop: wrap your root composable

Kotlin
@Composable
fun App() {
    MaterialTheme {
        KmpInspector {
            MyAppContent()
        }
    }
}

// Network, any platform
HttpClient { install(KmpInspectorPlugin) }

// Room, any platform
RoomInspector.attach(database, fileName = "app.db")

Avoid two bubbles on Android. If your shared UI wraps KmpInspector { } and Android also calls install, turn the wrapper off on Android with its enabled parameter.

A tour of the panels

Network

The list shows the newest requests first, each with its status, method, path, time and size, and a bar that makes slow calls stand out. Tapping a request opens Headers, Request and Response tabs. JSON responses appear as a tree you can expand and collapse, and any request can be copied as a cURL command.

KMP Inspector Network panel on a phone listing requests with 200, 201, 304, 409, 500 and error statuses
Requests with status, timing and size, and filters for errors, slow calls and writes.
KMP Inspector request detail showing a JSON response as an expandable tree and a copy as cURL link
A request's details, with the JSON response as a tree.

Logs

Each log line shows its level (verbose, debug, info, warning or error) and its tag, and you can filter by either. The list follows new lines as they arrive, and you can pause it while you read. On Android the app's logcat flows in automatically; elsewhere you log through InspectorLog, for example InspectorLog.e("Checkout", "Failed to parse price"). The filtered lines can be shared as text.

Crashes

Fatal crashes are saved and still there after the app restarts. Handled exceptions can be recorded with Inspector.recordNonFatal(...), and repeated crashes are grouped with a count. In a stack trace, frames from your own package are highlighted, framework frames can be hidden, and the whole trace can be copied or shared.

KMP Inspector Logs panel with level filters and log lines from several tags
Logs filtered by level, following new lines as they arrive.
KMP Inspector Crashes panel listing a fatal crash that happened seven times and two caught exceptions
A fatal crash seen seven times, and two caught exceptions.

The Database panel gets its own article: how to inspect a Room database in Kotlin Multiplatform.

Keeping it out of release builds

The inspector can read your database and run queries against it, so it must not ship switched on. There are two separate questions: whether the inspector runs, and whether its code is in the binary at all.

Stopping it from running

One switch covers every platform. With it off, nothing is captured, no crash handler is installed, no tables are read, and the wrapper renders your content directly:

Kotlin
Inspector.enabled = isDebugBuild // once, at startup

Removing the code: debugImplementation in Kotlin Multiplatform

  • Android: R8 removes it. Use the debugImplementation and releaseImplementation split shown above, so release builds only contain the no-op.
  • iOS: Kotlin/Native removes unused code, unless your framework exports the library. Do not export(...) it or turn on transitiveExport.
  • Desktop: nothing is stripped on the JVM, so ship a distribution that depends on the no-op instead.

A shared Kotlin Multiplatform module has no debugImplementation, but the choice can still be made at build time. Xcode passes its build configuration in the CONFIGURATION environment variable, and a Gradle property covers desktop:

shared/build.gradle.kts
val inspectorEnabled = System.getenv("CONFIGURATION") != "Release" &&
    providers.gradleProperty("inspectorEnabled").getOrElse("true").toBoolean()

sourceSets {
    commonMain.dependencies {
        if (inspectorEnabled) {
            implementation("io.github.waqas028:kmp-inspector:1.0.0")
        } else {
            implementation("io.github.waqas028:kmp-inspector-no-op:1.0.0")
        }
    }
}

The Ktor and Room companions have no no-op version, because they depend on the real library. Put them behind the same flag, and keep their call sites in code that is only compiled when the inspector is on.

How I use it in my own apps

I use KMP Inspector in my own apps. Dukaan POS, a point of sale for Android, iOS, desktop and the web, uses the library with both companions. KMP Inspector does not publish a browser version, so Dukaan keeps it in a source set shared only by Android, iOS and desktop. On Android it relies on install, and it wraps the UI only on the other platforms.

Shared Khata, my expense-splitting app, uses the debug and no-op split on Android, calls install and attaches its Room database, and records network calls through the OkHttp interceptor inside Ktor's OkHttp engine. On iOS it wraps its main view controller in KmpInspector { }.

Limitations

  • There is no web (JavaScript or Wasm) version yet.
  • On iOS, crash capture covers uncaught Kotlin exceptions only; Objective-C and Swift exceptions and hard signals do not reach it.
  • The Background Work panel is Android only.
  • Off Android, network and database data come from the Ktor and Room companions or from your own calls to the API.
  • The Network panel keeps the most recent 200 requests, and the Logs panel the most recent 2,000 lines.

Frequently asked questions

Is KMP Inspector free and open source?

Yes. It is open source under the Apache 2.0 license, and the code is on GitHub.

Does it work in an Android app that is not Kotlin Multiplatform?

Yes. KmpInspector.install(this) works in any Android app, including apps built with XML layouts and Fragments.

Can I use it with SQLDelight or another HTTP client?

Yes. Report requests with Inspector.recordRequest(...) and database tables with Inspector.setDatabase(...). The Ktor and Room companions just do this for you.

Will it end up in my release build?

Not if you follow the release section above: depend on the no-op artifact for release builds, and set Inspector.enabled from your debug flag.

Summary

Kotlin Multiplatform lets you share your code across platforms. KMP Inspector lets you share your debugging tools too: one bubble, five panels, on Android, iOS and desktop, with the release build left clean.

Try it from GitHub, and if something does not work as described, please open an issue there. If you need help building a cross-platform app, I work as a Kotlin Multiplatform developer.

Keep reading

All posts →