api_track_inspector

Inspect every API call in minutes, not hours. api_track_inspector adds a draggable in-app monitor with request details, timing, errors and export — and depends on nothing but the Flutter SDK.

dependencies:
  api_track_inspector: ^2.0.0

pub package License: MIT

Zero dependencies, on purpose

A debugging tool should never dictate your architecture. As of 2.0.0 this package has an empty dependencies: block — no GetX, no state-management library, no sizing library, no sharing plugin.

That means it:

  • works the same under Bloc, Riverpod, Provider, GetX, signals or plain setState;
  • needs no navigatorKey, no service locator, no ScreenUtilInit wrapper;
  • can never drag your transitive dependency versions around.

Upgrading from 1.x? See Migrating from 1.x — it is a small, mechanical change.

Screenshots

The carousel at the top of this package on pub.dev uses the same five images (from pubspec.yaml screenshots).

Preview (order: list → POST detail → bodies → error → 403 response):

Request list POST request detail Request and response bodies

Error detail 403 response and headers

Setup

1. Initialize

import 'package:flutter/foundation.dart';
import 'package:api_track_inspector/api_track_inspector.dart';

void main() {
  NetworkInspector.init(enabled: kDebugMode);
  runApp(const MyApp());
}

Keep it off in production. Logs hold full request and response bodies, including credentials, and the export button can copy them out of the app. Gate on kDebugMode, a dev flavor, or both.

2. Mount the FAB

MaterialApp(
  builder: (context, child) => NetworkInspector.wrapWithFAB(child!),
);

Works identically with MaterialApp.router, GoRouter, or any other routing setup. When enabled is false, wrapWithFAB returns child untouched, so it is safe to leave in release builds.

3. Log your traffic

Two calls: open a log on request, close it on response.

Dio

Copy this interceptor into your project (the package does not ship it — that would mean depending on Dio):

class InspectorInterceptor extends Interceptor {
  static const _logIdKey = '_inspectorLogId';
  static const _startKey = '_inspectorStart';

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    final logId = NetworkInspector.logRequest(
      method: options.method,
      url: options.uri.toString(),
      headers: Map<String, dynamic>.from(options.headers),
      body: options.data,
    );
    if (logId != null) {
      options.extra[_logIdKey] = logId;
      options.extra[_startKey] = DateTime.now();
    }
    handler.next(options);
  }

  @override
  void onResponse(Response<dynamic> res, ResponseInterceptorHandler handler) {
    _complete(res.requestOptions, statusCode: res.statusCode, body: res.data);
    handler.next(res);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    _complete(
      err.requestOptions,
      statusCode: err.response?.statusCode,
      body: err.response?.data,
      error: err.message ?? err.type.name,
    );
    handler.next(err);
  }

  void _complete(
    RequestOptions options, {
    int? statusCode,
    dynamic body,
    String? error,
  }) {
    final logId = options.extra[_logIdKey] as String?;
    final start = options.extra[_startKey] as DateTime?;
    if (logId == null) return;
    NetworkInspector.logResponse(
      logId: logId,
      statusCode: statusCode,
      body: body,
      duration: start == null ? null : DateTime.now().difference(start),
      error: error,
    );
  }
}

Register it last in your interceptor chain, so it observes headers your auth interceptor has already attached.

package:http (or anything else)

final logId = NetworkInspector.logRequest(method: 'GET', url: uri.toString());
final startedAt = DateTime.now();

final response = await http.get(uri);

if (logId != null) {
  NetworkInspector.logResponse(
    logId: logId,
    statusCode: response.statusCode,
    body: response.body,
    duration: DateTime.now().difference(startedAt),
  );
}

Opening the inspector

  • The FAB — draggable, snaps to the nearest edge, tap to open. Tap the barrier, the close button, or the FAB again to dismiss.
  • From your own codeNetworkInspector.show(context) pushes it as a normal dialog route, so the system back button closes it.
IconButton(
  icon: const Icon(Icons.bug_report),
  onPressed: () => NetworkInspector.show(context),
);

context must be under a Navigator — any widget inside your screens qualifies. A context taken from MaterialApp.builder does not, because that builder runs above the Navigator; that is exactly why the FAB presents the inspector itself instead of pushing a route.

Sharing and export

By default the export and share buttons copy JSON to the clipboard, so the package needs no sharing plugin. To use a native share sheet, pass a handler:

NetworkInspector.init(
  enabled: kDebugMode,
  onShare: (data, subject) => SharePlus.instance.share(
    ShareParams(text: data, subject: subject),
  ),
);

Configuration

Option Default Purpose
enabled true Master switch. False makes every API a no-op.
maxLogs 100 Ring-buffer size; oldest entries are dropped.
primaryColor / secondaryColor Blue FAB and header gradient.
fabBottomPosition / fabRightPosition 100 / 16 Starting FAB position.
showShareButton true Per-log share button.
onShare null Share handler; falls back to the clipboard.

API

Member Purpose
NetworkInspector.init(...) Configure and start.
NetworkInspector.wrapWithFAB(child) Mount the draggable FAB.
NetworkInspector.show(context) Open as a dialog route.
NetworkInspector.logRequest(...) Open a log; returns its id, or null when disabled.
NetworkInspector.logResponse(...) Close a log by id.
NetworkInspector.clearLogs() Drop all logs.
NetworkInspector.exportLogs() All logs as a JSON string.
NetworkInspector.service The NetworkInspectorService (a ChangeNotifier).
NetworkInspector.maybeService Same, but null instead of throwing.
NetworkInspector.reset() Tear down and drop logs (useful in tests).

NetworkInspectorService is a plain ChangeNotifier, so you can build your own UI on it:

ListenableBuilder(
  listenable: NetworkInspector.service,
  builder: (context, _) => Text('${NetworkInspector.service.logs.length}'),
);

Migrating from 1.x

2.0.0 removes GetX. Everything below is mechanical:

1.x 2.0.0
NetworkInspector.showDialog() NetworkInspector.show(context)
NetworkInspector.onRequest(request) (GetConnect) Removed — use logRequest / logResponse
NetworkInspector.onResponse(req, res) (GetConnect) Removed — as above
NetworkInspector.setRequestBody(body) Removed — pass body: to logRequest
Share used share_plus automatically Pass onShare:, or accept the clipboard fallback
Host app needed ScreenUtilInit No longer required
NetworkInspectorService extends GetxService extends ChangeNotifier

GetConnect users: the two convenience wrappers are gone because they took GetX types in their signature. Call logRequest / logResponse from your own request and response modifiers instead — same two calls as every other client.

If you still want the FAB and the GetX-based dialog, pin api_track_inspector: 1.1.18.

Requirements

  • Flutter >=3.27.0, Dart >=3.6.0.

License

MIT

Libraries

api_track_inspector
Network Inspector — a dependency-free API debugging tool for Flutter.