flutter_api_inspector 0.3.2
flutter_api_inspector: ^0.3.2 copied to clipboard
Debug-first in-app HTTP inspector for Flutter mobile/desktop. Live timeline, route grouping, details, and QA force-enable support without proxy setup.
flutter_api_inspector #
FAB → Timeline → Routes → Endpoint panel — all inside your running debug app.
Debug-first in-app overlay that intercepts HTTP traffic and shows a live timeline with status codes, durations, and response bodies — without leaving the app, without proxy certificates, and without modifying a single API call.
By default, the overlay is guarded by kDebugMode and omitted from
normal release usage. You can opt into controlled QA builds with
ApiTrace.forceEnabled; route-scoped tracking and route endpoint panels
remain debug-only in the current implementation.
Why #
When an endpoint misbehaves (wrong status, slow response, unexpected
body), Flutter developers waste time sprinkling debugPrint calls,
rebuilding, and reading scrolling console output. There is no in-app
visualization that shows "the last N calls, in order, with the bodies
I care about."
flutter_api_inspector fills that gap with a floating action button
that opens a timeline panel inside your running debug app.
Quickstart #
Add the dependency:
dependencies:
flutter_api_inspector: ^0.3.2
Option A — Automatic capture (recommended) #
Install TracedHttpOverrides once in main(). It intercepts every
dart:io HttpClient constructed anywhere in the process — this covers
GetConnect, package:http, Dio, and any other client that uses
dart:io under the hood:
import 'dart:io';
import 'package:flutter_api_inspector/flutter_api_inspector.dart';
void main() {
HttpOverrides.global = TracedHttpOverrides();
ApiTrace.runApp(const MyApp());
}
One import covers everything — overlay, manual API, and both interceptors. The package targets mobile / desktop, where there is no browser console; on Flutter Web use the browser's own DevTools Network tab instead.
That's it. Every HTTP call your app makes will appear in the overlay with its URL, method, status code, duration, and response body.
Option B — Manual instrumentation #
Wrap individual calls with ApiTrace.call(...) for fine-grained
control over what is captured and what label appears in the timeline:
import 'package:flutter_api_inspector/flutter_api_inspector.dart';
await ApiTrace.call(
'POST authenticator/v2',
method: 'POST',
url: Uri.parse('$baseUrl/pxauth/authenticator/v2'),
execute: () async {
final res = await post(url, body, contentType: 'application/json');
return ApiTraceResponse(
statusCode: res.statusCode ?? 0,
responseBody: res.bodyString,
);
},
);
Option C — Dio interceptor #
If you use Dio and want per-instance
control, add TracedDioInterceptor instead of (or alongside)
TracedHttpOverrides:
import 'package:dio/dio.dart';
import 'package:flutter_api_inspector/flutter_api_inspector.dart';
final dio = Dio()..interceptors.add(TracedDioInterceptor());
Installing both is safe: on mobile/desktop Dio routes through
dart:io HttpClient, so TracedDioInterceptor detects that
TracedHttpOverrides is active and defers to it automatically —
each call is recorded exactly once. If your Dio instance uses a
custom HttpClientAdapter that bypasses dart:io (mocks, http2),
pass TracedDioInterceptor(deferToHttpOverrides: false) so the
interceptor records those requests itself.
Note: Both interceptors are exported from the main barrel (
flutter_api_inspector.dart).diois a transitive dependency of this package regardless of which interceptor you use. If you only needTracedHttpOverridesand want to avoid pulling indio, import directly frompackage:flutter_api_inspector/interceptor/http_overrides.dart.
Route-Scoped Network Inspector #
New in 0.3.0. The overlay correlates every HTTP call with the Flutter screen (route) that was active when it was dispatched, so instead of a flat chronological timeline you get: "CheckoutScreen → POST /orders (500) → GET /payment-methods (timeout)".
ApiTrace.runApp auto-injects a RouteScopeObserver into your
MaterialApp's navigatorObservers (debug-only, no setup required).
TracedHttpOverrides and TracedDioInterceptor automatically tag
every request with the active route's scopeId and name.
Open the overlay panel and switch to the Routes tab to:
- Browse a hierarchical tree of every screen visited this session (indented by navigation depth, with call/error counts per screen).
- Tap a screen to filter the Timeline tab down to just its calls,
with timestamps shown relative to screen entry (
+230ms,+1.4s). - Open a route's endpoint list from the
list_alt/ View endpoints action; the<routeName> Endpointspanel lists each endpoint, and tapping one opens itsApiTraceDetailScreen. - Export just that screen's calls as cURL (
Copy as cURLincludes a// Route: <name> (scopeId: <id>)header).
MaterialApp.router users #
The auto-injection only wires into BootstrapMaterialAppHarness's
own MaterialApp. If your app uses MaterialApp.router (go_router,
auto_route, etc.), add RouteScopeObserver to your router's
observers list yourself:
GoRouter(
observers: [RouteScopeObserver()],
routes: ...,
);
Manual instrumentation + route scoping #
If you call ApiTrace.call(...) directly instead of using an
automatic interceptor, tag it yourself by reading RouteScope.current
and forwarding it through extra — the same convention the built-in
interceptors use internally:
final scope = RouteScope.current;
await ApiTrace.call(
'orders.create',
method: 'POST',
url: Uri.parse('$baseUrl/orders'),
extra: <String, Object?>{
if (scope != null) 'routeScopeId': scope.scopeId,
if (scope != null) 'routeName': scope.routeName,
},
execute: () async => ApiTraceResponse(statusCode: 201),
);
Read the scope data back off any record via the ApiTraceRecordScope
extension: record.routeScopeId / record.routeName (both null for
calls made before route tracking was active, or for legacy records).
Route names come from Route.settings.name, falling back to
route.runtimeType.toString() for unnamed routes — name your routes
for readable labels in the Routes tab.
Public API surface #
// Master switch + global config.
ApiTrace.enabled; // bool, defaults to kDebugMode
ApiTrace.config; // ApiTraceConfig, mutable
ApiTrace.timeline; // in-memory ring buffer (Timeline)
// Capture one call manually.
Future<String?> ApiTrace.call(
String name, {
required String method,
required Uri url,
required Future<ApiTraceResponse> Function() execute,
Set<ApiTraceDetail>? detailOverride,
Map<String, Object?>? extra,
});
// One-line bootstrap.
ApiTrace.runApp(Widget app);
// Route-scoping (see "Route-Scoped Network Inspector" above).
RouteScope.current; // RouteScopeEntry? — the active screen's scope
RouteScope.entries; // List<RouteScopeEntry> — current route-flow history
RouteScopeObserver(); // NavigatorObserver, for MaterialApp.router
record.routeScopeId; // int? — via the ApiTraceRecordScope extension
record.routeName; // String?
Configuration #
All fields are optional — const ApiTraceConfig() gives the package
defaults:
ApiTrace.config = const ApiTraceConfig(
// Which fields to capture on every call.
// Default: {minimal, response} — captures status/duration/url
// plus the response body so failed calls are immediately readable.
details: {ApiTraceDetail.minimal, ApiTraceDetail.response},
// Overlay FAB position: bottomRight | bottomLeft | topRight | topLeft.
overlayPosition: ApiTraceOverlayPosition.bottomRight,
// FAB label: icon (default) | badge (count) | chip (text + count).
overlayLabel: ApiTraceOverlayLabel.icon,
// Whether the FAB can be dragged around the screen. Default true.
draggableFab: true,
// Gap between two calls before the panel groups them separately.
// Set to Duration.zero to disable grouping.
flowGroupGap: Duration(seconds: 2),
// Timeline panel height range (logical pixels).
panelMaxHeight: 600,
panelMinHeight: 360,
// Max bytes stored per response body (truncated beyond this).
maxResponseBodyBytes: 4096,
// Ring buffer capacity (oldest record evicted when full).
timelineCapacity: 200,
);
Release-mode testing (force enable) #
By default the overlay is guarded by kDebugMode for normal release
builds. When you need controlled QA coverage in a release build — e.g.
for a tester flavor or a feature-flag-gated development module — set
ApiTrace.forceEnabled = true before calling ApiTrace.runApp():
void main() {
if (moduloDesarrollo.isHabilitado) {
ApiTrace.forceEnabled = true;
}
HttpOverrides.global = TracedHttpOverrides();
ApiTrace.runApp(const MyApp());
}
The effective value of ApiTrace.enabled is _enabled || forceEnabled.
To temporarily disable the overlay on a force-enabled build, set both
forceEnabled = false and enabled = false.
forceEnabled constructs the ApiTrace.runApp bootstrap and overlay for
QA builds. Route-scoped tracking (RouteScope.init()) and the route
endpoint panel are still guarded by kDebugMode, so release-mode QA
builds should not rely on Routes tab endpoint drill-downs.
Detail levels #
| Value | Captures |
|---|---|
minimal |
URL, method, status code, duration (always on) |
response |
Response body (truncated to maxResponseBodyBytes) |
request |
Request body |
headers |
Request + response headers |
full |
Everything above |
TracedHttpOverrides — how it works #
TracedHttpOverrides replaces HttpClient.createHttpClient globally
(via HttpOverrides.global). Every HttpClient created anywhere in
the process — including inside GetConnect, package:http, and Dio's
default adapter — goes through TracedHttpClient.
When a request is closed, the wrapper:
- Captures the request body bytes as the caller writes them (only
when the detail set includes
requestorfull, capped atmaxResponseBodyBytes). - Reads the full response body into memory (up to
maxResponseBodyBytesfor the inspector; the complete body is replayed transparently to the caller). - Completes the
ApiTracerecord with status code, request/response headers, and decoded bodies. - Returns a
_ReplayHttpClientResponseso the caller receives every byte unchanged.
The interception is transparent: GetConnect, http, and Dio see no
difference in the response they receive.
Limitations of TracedHttpOverrides:
- Mobile and desktop only (
dart:io). Flutter Web usesXMLHttpRequest; installTracedHttpOverridesconditionally with!kIsWeb. - Eagerly buffers the full response before returning it to the caller. This is a non-issue for REST API payloads; avoid it for large file downloads in debug mode.
- Dio with a custom
HttpClientAdapter(e.g. mocks) bypassesdart:ioentirely — useTracedDioInterceptorin that case.
Limitations #
- Debug-first. Normal release builds leave tracing disabled, and
ApiTrace.callis a no-op whenApiTrace.enabledis false (the default in release).ApiTrace.forceEnabledis intended for controlled QA builds, not production diagnostics. - Route tooling is debug-only today. Route-scoped tracking and the
route endpoint panel are guarded by
kDebugMode; force-enabled release builds can show the overlay, but should not depend on Routes tab endpoint drill-downs. - In-memory only. The ring buffer resets on every app restart. No disk persistence, no file export in v1.
- Read-only detail view. No re-run, no replay. cURL export exists only when filtered to a specific route in the Routes tab (see "Route-Scoped Network Inspector" above) — there is no export for the full, unfiltered timeline in v1.
- No Flutter Web support. Web is intentionally out of scope: the
browser's DevTools Network tab already covers it. This package
exists for mobile / desktop, where there is no console. The
pubspec declares
platforms:accordingly, so pub.dev shows web as unsupported. - Single overlay per app. Multi-window is out of scope for v1.
License #
MIT — see LICENSE.