AppInspect
On this page
Getting started

Install

Two dependency lines get you a working inspector, with every feature on and no configuration to write. A third line on your OkHttpClient turns on network capture. Everything after that is optional.

Or start from the sample app

If you would rather see the inspector working before you touch your own project, clone AppInspect-Sample-App — a small Kotlin app that exists only to demonstrate the library. Run the debug variant, tap a few buttons to generate API calls, preference and database writes, a background job and a crash, then open the inspector and look at what it captured. About five minutes, and all you need is a device or emulator on Android 7.0+ and either Android Studio or JDK 17 on the command line.

Terminal
git clone https://github.com/appinspect/AppInspect-Sample-App.git
cd AppInspect-Sample-App
./gradlew :app:installDebug

It doubles as a worked example of everything below, because the whole integration is three files: the two dependency lines in app/build.gradle.kts, addAppInspectInterceptor() on the OkHttp builder, and a debug-only object holding the open() and updateConfiguration() calls — the pattern described under one thing to guard by variant. Every touch point is commented. The rest of the app is ordinary Android code with no reference to AppInspect at all, which is the point: apart from the interceptor, the library observes your app rather than being called by it.

Before you start

Three things, and most projects already have all of them: minSdk 24, AndroidX, and build variants you can target separately — at minimum debug and release. OkHttp is needed only for the Network and Mocks panels; everything else works without it.

You do not need Compose, Kotlin, a DI framework, an Application subclass or a single manifest change. If you are checking whether it will fit an existing app, that list is on compatibility.

Step 1 — resolve from Maven Central

AppInspect is published to Maven Central, so in most projects this is already in place. If your project declares repositories centrally, confirm mavenCentral() is there.

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

Step 2 — add the dependency per variant

Add the full library to the builds you test, and the no-op stub to the build you ship. Do not use plain implementation — that puts the inspector in your production APK.

app/build.gradle.kts
dependencies {
    // The full inspector, for builds your team tests
    debugImplementation("io.github.appinspect:appinspect:<latest-version>")

    // A pass-through stub, so shared OkHttp code still compiles in release
    releaseImplementation("io.github.appinspect:appinspect-no-op:<latest-version>")
}

That is enough to have a working inspector. AppInspect initialises itself through AndroidX Startup when the process starts, and everything is on by default — every panel, the shake gesture, the launcher shortcut, crash capture, notifications and the log tail. There is nothing to add to your Application class and no configuration to write.

Using an older group?

Earlier releases used the group io.github.suryansh1720001.appinspect. It still resolves — Maven Central never removes anything — but it receives no further releases, so a build pinned to it stays behind without ever failing. Change the group to io.github.appinspect. Artifact IDs, package names and the public API are unchanged, so the dependency lines are the only thing that moves.

If you have more than two variants

Gradle only generates debugImplementation and releaseImplementation automatically. For your own variants, add the configuration by name and give the build type matchingFallbacks — AppInspect publishes only debug and release variants, so without it Gradle cannot resolve the dependency at all:

app/build.gradle.kts
android {
    buildTypes {
        create("staging") {
            initWith(getByName("release"))
            matchingFallbacks += listOf("release", "debug")
        }
    }
}

dependencies {
    val group = "io.github.appinspect"
    val inspector = "$group:appinspect:<latest-version>"

    debugImplementation(inspector)
    add("stagingImplementation", inspector)
    add("internalTestImplementation", inspector)

    releaseImplementation("$group:appinspect-no-op:<latest-version>")
}
Adding the artifact is not the same as letting it run

The library checks the build itself, and a variant that is not debuggable stays completely off until you opt in — which is deliberately not something host code can do. A variant like the one above inherits isDebuggable = false from release, so it needs one more line. That, and every other build shape, is on builds and environments.

Step 3 — capture network traffic

Network capture is opt-in, because AppInspect never touches an OkHttpClient you did not hand to it. Add the extension to your builder, and add it last:

Your OkHttp setup
val client = OkHttpClient.Builder()
    .addInterceptor(authInterceptor)
    .addInterceptor(loggingInterceptor)
    .addAppInspectInterceptor()   // last
    .build()

The same code compiles in every variant. In release the extension comes from appinspect-no-op and returns the builder untouched, so there is no if (BuildConfig.DEBUG) to write.

If something else builds the client for you — Retrofit, Ktor, or any other library sitting on OkHttp — that same line is still the whole integration. It just goes in a slightly different place; see Retrofit, Ktor, and other clients built on OkHttp below.

Why the order matters

Two reasons, and both are easy to get wrong.

Short-circuit mocks skip whatever comes after. When a SHORT_CIRCUIT rule fires, AppInspect returns a response without calling chain.proceed(), so every interceptor registered after it never runs. If your token-refresh interceptor sits after AppInspect, mocking will quietly bypass it. Last means nothing of yours gets skipped.

Last is also where the request looks the way it really goes out — after your own interceptors have added their headers.

You get the real wire headers

addAppInspectInterceptor() actually installs two interceptors: an application-level one that does the capturing and the mocking, and AppInspectOkHttpWireHeaderInterceptor at the network level, which only observes.

The second one exists because an application interceptor cannot see headers added downstream of itself — an Authenticator retry, another network interceptor, or OkHttp's own BridgeInterceptor, which adds Host, User-Agent, Accept-Encoding, Content-Length, Content-Type and Cookie. Without it, an Authorization header attached later in the chain would simply be missing from the panel. With it, what you see matches what Android Studio's Network Inspector shows.

Where wire headers genuinely do not exist — a short-circuited mock, a fully cached response, a call that failed before reaching the network — the event falls back to the application-level headers. This also means captured request headers can contain credentials your calling code never set, which matters when you share an export: see the QA checklist.

Retrofit, Ktor, and other clients built on OkHttp

Nothing above is specific to calling OkHttp directly. Capture is an ordinary OkHttp interceptor, so the rule is generic: if OkHttp is the engine underneath, AppInspect can see the traffic — whatever library sits on top of it. Instrument the OkHttpClient your stack actually uses and everything on this page still holds: the ordering rule, the wire headers, mocking, and the no-op substitution in release.

Two shapes cover almost every library:

  • It takes a client you built — Retrofit, Apollo, Coil, Glide's OkHttp loader. Instrument the builder exactly as above and hand the finished client over: Retrofit.Builder().client(instrumentedClient). Nothing else to do.
  • It builds its own and exposes the builder — Ktor on the OkHttp engine. Reach the builder one level down, through engine { config { … } }.

If OkHttp is not underneath — Volley, Cronet, HttpURLConnection, a WebView, or Ktor on a non-OkHttp engine — there is nothing to hook, and the Network and Mocks panels stay empty. No other panel is affected either way; none of them involve HTTP.

Ktor is the one worth a snippet, since it needs io.ktor:ktor-client-okhttp and the extension goes a level deeper than you might expect:

Your Ktor client
val client = HttpClient(OkHttp) {
    install(ContentNegotiation) { json() }

    engine {
        addInterceptor(authInterceptor)      // your own, first
        config {
            addAppInspectInterceptor()      // last
        }
    }
}
  • Order is preserved. addInterceptor, addNetworkInterceptor and config { } all append to the same builder in the order you write them, so last still means last.
  • Already build a client elsewhere? Instrument that builder and pass the result as engine { preconfigured = instrumentedClient } instead. Same outcome.
  • Mocking works. A mock is served from inside OkHttp, so Ktor never learns the difference.
  • Ktor's other engines capture nothing. CIO, Android, Java and Darwin are not OkHttp and expose no interceptor. Switching engine purely to get capture changes your networking stack — your call to make, not a recommendation.
  • A redirect is several events, not one. The engine leaves redirects to Ktor's HttpRedirect plugin, which sits above OkHttp, so each hop is captured separately — as is each HttpRequestRetry attempt.
  • Streaming and multipart request bodies show as omitted. Ktor sends those as one-shot bodies that can be read exactly once, and draining one to display it would break the request. The event is otherwise complete; JSON, form and text bodies are captured normally, and response bodies are never affected on any engine.

Step 4 — check that it worked

  1. Install and launch a debug build.
  2. Shake the device firmly. The inspector should open full-screen. If shaking feels unreliable on your device, other entry points work just as well.
  3. Trigger a network call in your app, then open the Network panel. The call should be listed with its method, path, status and duration.

If the inspector opens but Network is empty, the interceptor is not on the client that made the call — a second OkHttpClient built somewhere else is the usual culprit, and on Ktor it is usually a client that is not on the OkHttp engine. If nothing opens at all, the build is almost certainly not debuggable: see builds and environments.

Optional — change a default

Auto-initialisation uses the defaults, and it has already happened by the time your Application.onCreate() runs. To adjust something — hide a panel, make storage read-only, silence notifications — change only that:

DebugApplication.kt (debug source set)
AppInspect.updateConfiguration { configuration ->
    configuration.copy(
        panels = AppInspectPanels(mocksEnabled = false),
    )
}

Every field is documented on the configuration reference, along with why updateConfiguration is the safer of the two entry points: AppInspect.install() replaces the configuration wholesale, which throws away the build-tier enablement AppInspect worked out for the variant.

One thing to guard by variant

appinspect-no-op deliberately mirrors only the OkHttp API surface. AppInspect.install(), updateConfiguration(), open() and the long-press trigger helpers do not exist there, so calling them from shared code breaks the release build.

Keep those calls in a debug-only source set, or behind your own no-op indirection. The interceptor line is the only part designed to live in shared code.

Notifications

AppInspect can post a notification for each captured call, which is handy for watching traffic without keeping the inspector open. On Android 13 and newer your app has to request the runtime permission itself — see Network notifications.