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.
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.
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.
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.
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:
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>")
}
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:
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
OkHttpengine. Reach the builder one level down, throughengine { 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:
val client = HttpClient(OkHttp) {
install(ContentNegotiation) { json() }
engine {
addInterceptor(authInterceptor) // your own, first
config {
addAppInspectInterceptor() // last
}
}
}
-
Order is preserved.
addInterceptor,addNetworkInterceptorandconfig { }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,JavaandDarwinare 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
HttpRedirectplugin, which sits above OkHttp, so each hop is captured separately — as is eachHttpRequestRetryattempt. - 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
- Install and launch a debug build.
- Shake the device firmly. The inspector should open full-screen. If shaking feels unreliable on your device, other entry points work just as well.
- 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:
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.
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.