πŸ” Flutter Inspector

pub package platform

Language / θͺžθ¨€: English | 繁體中文

In-app, multi-inspector debugging overlay for Flutter apps β€” logs, network, navigation, and database, all behind one unified API.

πŸ“¦ Features

Feature Description Use Case Example
πŸͺ΅ Console Capture logs across five severity levels (verbose / debug / info / warning / error), with optional structured data and stack traces QA says "tapping checkout does nothing" β€” open Console, spot a red error entry in the timeline; tap in to see the structured error detail, response body, and full stack trace to understand what went wrong
🧡 Merged Timeline Console tab interleaves logs, network, navigation, and database events on one timestamp-sorted timeline, with per-source filter chips Continuing the checkout case β€” switch the source chip to "All" and scroll back along the timeline to inspect the request that got 401: check what token was in the Authorization header, what params were sent, and compare with backend expectations to pinpoint why the server rejected it β€” all without switching tabs or manually comparing timestamps
πŸ“‘ Network Intercept HTTP traffic via Dio; inspect structured request/response details; search/filter by URL, method, or status; share as cURL A page shows up completely blank β€” open the Network tab to find the API returned an error, so there's no data to display; tap in to inspect request params and response body, then copy as a runnable cURL command and paste it into a bug ticket for the backend team to reproduce
πŸ”„ Network Replay Resend a captured request using the original Dio instance (same headers, base URL, interceptors); replayed entries are auto-labeled Re-trigger a failed API call on-device to verify a server-side hotfix without restarting the app or rebuilding the user flow
🚨 Structured Network Errors Failed requests show an Exception Details section distinguishing transport-layer failures (device offline / DNS / timeout) from server-side errors (4xx/5xx), with copyable stack traces Instantly tell whether "Failed" means the device lost connectivity or the server returned 500 β€” no more guessing during QA
πŸ“Š Error Aggregation Summary Network tab shows a collapsible banner that groups failed/errored requests by status code (or error type for transport failures), with per-group counts and time range; tap a group to filter the call list down to just that error A page is flooded with dozens of failed calls β€” glance at the summary banner to see "12Γ— 401", "3Γ— timeout", tap the 401 group to isolate exactly those calls instead of scrolling through the full list
🩺 Diagnostic Report Export a single Markdown report β€” device/app header, current route stack, and the logs / network / navigation / database sections β€” filtered by time window (5m / 1h / all), source, and an optional errors-only toggle; straight to the share sheet, nothing written to disk QA reproduces a bug and, instead of screenshotting four tabs and hand-typing the OS version, taps Export once and pastes a complete report into the Jira ticket
🌐 WebView Inline Debugging Bridge a WebView's own console.*, window.onerror/unhandledrejection, and fetch/XMLHttpRequest activity into the same Console and Network tabs used for native logs and Dio traffic β€” dependency-free, wire your own webview_flutter or flutter_inappwebview to WebViewBridgeAdapter A hybrid screen misbehaves and you can't tell if the bug is in Flutter or the embedded web page β€” see the WebView's JS errors and its fetch/XHR calls inline in Console/Network, tagged by origin, so you know instantly which side to fix without attaching Chrome DevTools to the WebView
πŸ›‘οΈ Sensitive-Data Redaction Secure by default β€” sensitive headers (Authorization, Cookie, Set-Cookie, X-Api-Key) are masked in every share/export path Safely share network logs with teammates or attach them to Jira tickets without leaking tokens or session cookies
🧭 Navigator Track route pushes, pops, and replacements automatically; toggle between Event History (raw log) and Active Stack (live route-stack visualization) Verify deep-link routing, confirm back-stack correctness, or diagnose "why did the user land on this screen?" during a QA walkthrough
πŸ—„οΈ Database Record insert / update / delete / query operations with affected-row counts and payloads; browse real tables via pluggable DatabaseBrowserSource (SQLite / ObjectBox adapters provided) Verify that a "Save" action actually wrote the expected rows; browse local SQLite tables on-device without pulling the .db file
πŸ›‘ Uncaught Error Capture (opt-in) Automatically turn uncaught errors into error-level Console logs via three Flutter hooks (build/layout/paint, async, ErrorWidget); chains existing handlers β€” never swallows errors An unawaited Future throws deep inside a third-party package β€” no try/catch anywhere near it. Uncaught error capture logs it automatically with a full stack trace, so it shows up in Console without any manual instrumentation
πŸ”” Live Notification (opt-in) A system notification summarising the latest API call and the running total; tap to jump straight to the Network tab Monitor API traffic in real-time while navigating the app β€” no need to keep the dashboard open; also useful for verifying whether the number of API calls per operation is reasonable (e.g., a single page load triggering dozens of calls hints at redundant requests)
πŸ‘† Magical Tap & Floating Button Open the dashboard with a hidden multi-tap gesture or a draggable in-app FAB Embed in a release build as a hidden diagnostic entry point β€” when QA or users hit an issue, trigger the inspector on the spot for initial error triage without rebuilding a debug version or tracing through source code
🧩 Custom Tab Inject your own widget as a 5th dashboard tab via FlutterInspector(customTab: ..., customTabTitle: ...) β€” sits alongside Console / Network / Navigator / Database Surface app-specific debug tooling right next to the built-in inspectors β€” a feature-flag toggle panel, the current auth/session state, or a "clear cache" button β€” so your team's ad-hoc diagnostics live in one place instead of a separate debug screen

πŸ“± Screenshots

Home Console Network
Network Detail Navigator Uncaught Error
Database Browse

πŸͺš Usage

Add to pubspec.yaml

dependencies:
  flutter_inspector_kit: ^1.7.1

Then run flutter pub get.

Initialize

Create a single shared FlutterInspector instance and wire it into your app. Register the navigator observer to track routes, and wrap your app in FlutterInspectorMagicalTap so a hidden gesture can open the dashboard from anywhere.

import 'package:flutter/material.dart';
import 'package:flutter_inspector_kit/flutter_inspector_kit.dart';

final inspector = FlutterInspector();

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 1. Track navigation events
      navigatorObservers: [inspector.navigatorObserver],
      // 2. A hidden gesture opens the dashboard from anywhere
      builder: (context, child) {
        return FlutterInspectorMagicalTap(
          onTap: () => inspector.openDashboard(context),
          child: child ?? const SizedBox.shrink(),
        );
      },
      home: const MyHomePage(),
    );
  }
}

That's it? Yes, that's it.

Floating button

Prefer a visible trigger? Attach the inspector once the first frame is built to show a draggable floating button that opens the dashboard.

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    inspector.attach(context: context);
  });
}

Remove it again with inspector.detach().

Add a custom tab

Inject your own widget as a 5th dashboard tab, sitting alongside the built-in Console / Network / Navigator / Database tabs. Use it to surface app-specific diagnostics β€” a feature-flag panel, the current auth state, a "clear cache" button, etc.

final inspector = FlutterInspector(
  customTab: const MyDebugPanel(),
  customTabTitle: 'Flags', // defaults to 'Custom'
);

The widget is built lazily when its tab is first shown and can be any Flutter widget β€” including stateful ones with their own controllers.

Log network requests

With Dio

Add the interceptor to your Dio instance and every request/response is captured automatically. Pass the sourceDio instance to enable the Resend (Replay) feature in the Network detail view.

final dio = Dio();
dio.interceptors.add(FlutterInspectorDioInterceptor(inspector, sourceDio: dio));
Multiple Dio Instances

If your app uses multiple Dio instances (e.g., authDio for authenticated API calls, publicDio for public assets), register the interceptor on each instance and make sure to pass the respective instance as sourceDio:

// Authenticated API client
final authDio = Dio();
authDio.interceptors.add(FlutterInspectorDioInterceptor(inspector, sourceDio: authDio));

// Public API client
final publicDio = Dio();
publicDio.interceptors.add(FlutterInspectorDioInterceptor(inspector, sourceDio: publicDio));

This guarantees that replaying a request in the Network detail view uses the exact same Dio instance, maintaining the correct baseUrl, interceptors, and authentication state.

With other HTTP clients

Build a NetworkEntry yourself and pass it in:

inspector.logNetwork(entry);

To show an in-flight request that later resolves, log the pending entry first, then log the completed one with replaces so it updates in place instead of duplicating:

final pending = inspector.logNetwork(NetworkEntry(method: 'GET', url: url));
// ...after the response arrives:
inspector.logNetwork(completedEntry, replaces: pending);

Inside the Network tab

  • Search & filter: filter the call list by URL, method, or status code (case-insensitive); method and status (2xx/3xx/4xx/5xx/Failed) chips narrow it further.
  • Error summary banner: a collapsible banner above the call list groups failed/errored requests by status code (or error type for transport failures β€” offline/timeout/DNS), showing a per-group count and first/last-seen time range. Tap a group card to filter the list down to just that error; tap again to clear the filter.
  • Call details: tap any call for a structured view β€” General (method, URL, status with color coding, duration, request/response sizes), Query Parameters, Headers, and JSON-pretty bodies. Truncated bodies are clearly marked. Failed requests display an Exception Details section distinguishing between transport-layer failures and server-side errors, with a copyable stack trace.
  • Sharing: copy the call as a runnable cURL command, copy the full details as text, or open the system share sheet (native via share_plus, web via the browser Web Share API β€” falls back to the clipboard when unavailable).
  • Replay / Resend: for requests captured with a sourceDio provided to the interceptor, you can trigger a "Resend" action in the detail view to replay the request locally using the same Dio client (carrying the same headers, base URL, and interceptors). Replayed requests automatically show up as new entries with a dedicated "Replay" label.

Redact sensitive headers

Every share/export path in the Network detail view β€” copy as cURL, copy as text, and the system share sheet β€” masks sensitive headers by default, so secrets never leak to the clipboard, a share sheet, or a screenshot. The masked keys are Authorization, Cookie, Set-Cookie, and X-Api-Key (matched case-insensitively); their values are replaced with β€’β€’β€’β€’.

This is controlled by the redactSensitiveData constructor flag, which defaults to true:

// Secure by default β€” sensitive headers are masked in shared/exported output.
final inspector = FlutterInspector();

// Opt out (e.g. internal builds where you need the raw values).
final inspector = FlutterInspector(redactSensitiveData: false);

The flag only affects shared/exported text β€” the headers shown live inside the dashboard are always the real values.

WebView inline debugging

Bridge a WebView's own console.*, window.onerror/unhandledrejection, and fetch/XMLHttpRequest activity into the same Console and Network tabs used for native logs and Dio traffic β€” one more event source, not a second system. The package stays dependency-free: you bring your own WebView package (webview_flutter or flutter_inappwebview) and wire it to WebViewBridgeAdapter yourself.

Every integration follows the same three steps:

  1. Create final adapter = WebViewBridgeAdapter(inspector); β€” a translator that turns bridge messages into existing LogEntry / NetworkEntry objects via inspector.log / inspector.logNetwork. It holds no buffer and does no redaction of its own.
  2. Wire your WebView package's channel/handler (named via the kWebViewBridgeChannelName constant, i.e. 'FlutterInspectorBridge') to call adapter.handleMessage(rawMessageString).
  3. Inject the inspectorWebViewBridgeJs payload into the page β€” it hooks console.*, window.onerror, unhandledrejection, fetch, and XMLHttpRequest, and posts one JSON envelope per event back through the channel.

With webview_flutter

final adapter = WebViewBridgeAdapter(inspector);
controller
  ..addJavaScriptChannel(
    kWebViewBridgeChannelName, // 'FlutterInspectorBridge'
    onMessageReceived: (m) => adapter.handleMessage(m.message),
  )
  ..setNavigationDelegate(NavigationDelegate(
    onPageStarted: (_) => controller.runJavaScript(inspectorWebViewBridgeJs),
  ));

Working demo: example/lib/demos/webview_demo.dart.

With flutter_inappwebview

final adapter = WebViewBridgeAdapter(inspector);
// AT_DOCUMENT_START injection catches even the page's earliest logs.
controller.addUserScript(userScript: UserScript(
  source: inspectorWebViewBridgeJs,
  injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
));
controller.addJavaScriptHandler(
  handlerName: kWebViewBridgeChannelName,
  callback: (args) {
    // The page can call this handler directly β€” validate before forwarding.
    final raw = args.isNotEmpty ? args.first : null;
    if (raw is String) adapter.handleMessage(raw);
  },
);

Working demo: example/lib/demos/inappwebview_demo.dart β€” its page fires a log during document load, proving the document-start injection catches activity that webview_flutter's wiring misses.

Provenance: telling WebView traffic apart from native

Every bridged request is tagged first-class: NetworkEntry.origin is NetworkOrigin.webview (native Dio traffic defaults to NetworkOrigin.dio) and NetworkEntry.pageUrl carries the issuing page's location.href. Both show up as Origin / Page URL rows in the Network detail view. Bridged log entries carry the same marking in LogEntry.data (origin / pageUrl), visible in the log detail view's Data section.

Injection timing: know what each package can and can't catch

The two packages inject at genuinely different points, and that changes what you see:

  • flutter_inappwebview's UserScript with AT_DOCUMENT_START runs before the page's own scripts, so it catches logs/errors/requests fired during the page's earliest initialization.
  • webview_flutter's runJavaScript only runs from onPageStarted, which fires after navigation has already begun β€” any console.* / error / fetch activity before that point is missed. This is a real gap, not a rounding error; if you need the page's earliest activity, use flutter_inappwebview.

Limitations

  • Main frame only β€” iframes and Service Workers are not bridged; only the top-level page's JS is hooked.
  • setOnConsoleMessage is a console-only fallback β€” it doesn't cover errors or network activity, so it isn't the primary path here; the injected bridge JS is what drives all four event types.
  • Replay is unavailable for WebView network entries β€” the Network detail view's "Resend" action requires a sourceDio instance. WebView traffic isn't captured through Dio, so sourceDio is always null and Replay correctly stays disabled β€” the same degradation already used for any entry without a sourceDio.

Redaction

WebView network entries are ordinary NetworkEntry objects flowing through the same buffer as Dio-captured traffic, so they're masked by the exact same redactSensitiveData rules described above β€” same masked keys, same opt-out flag, same code path. There is no separate redaction step for WebView traffic to configure or forget.

Live notification (opt-in)

A continuously-updated system notification can summarise the latest call and the running total. It is disabled by default β€” enable it explicitly:

final inspector = FlutterInspector(showNetworkNotification: true);

Once enabled, the inspector requests notification permission for you when it initialises β€” the host app does not need to add any permission-handling code.

Notification behaviour:

  • Android: appears as a silent heads-up banner (no sound or vibration) when a new API call arrives. The banner animates in and dismisses automatically. Subsequent calls within a 2-second window silently update the notification content without re-alerting. After 2 seconds, the next call triggers another heads-up alert.
  • iOS / macOS: displays a foreground banner when a new API call arrives, throttled the same way as Android. This requires one line of setup in your AppDelegate β€” see Required iOS / macOS setup below. Without it, iOS silently suppresses the foreground banner (the entry is still delivered to Notification Center).
  • The notification uses a dedicated high-priority Android channel (flutter_inspector_network_v2) β€” if you upgrade from an earlier version, the old notification channel is automatically deleted and will not appear in system settings.

To make tapping the notification open the dashboard on the Network tab, pass a navigatorKey that is also wired into your MaterialApp:

final navigatorKey = GlobalKey<NavigatorState>();

final inspector = FlutterInspector(
  showNetworkNotification: true,
  navigatorKey: navigatorKey,
);

MaterialApp(navigatorKey: navigatorKey, /* ... */);

Without a navigatorKey the notification still shows; tapping it is simply a no-op since there is no navigation context to route from.

Android setup (required)

flutter_local_notifications relies on Java 8+ APIs, so your app's Gradle module must enable core library desugaring β€” this is needed whether or not notifications are enabled, otherwise the app will not build. In android/app/build.gradle.kts:

android {
    defaultConfig {
        multiDexEnabled = true
    }
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

Also ensure a notification icon exists at @mipmap/ic_launcher (default Flutter apps already have it). On Android 13+ the POST_NOTIFICATIONS runtime permission is requested automatically when the inspector initialises.

Required iOS / macOS setup

On iOS / macOS, the user is prompted for notification permission when the inspector initialises. The permission alone is not enough to show a banner while your app is in the foreground: the system only presents a foreground notification when a UNUserNotificationCenterDelegate returns it from willPresentNotification.

iOS Setup

FlutterAppDelegate on iOS already implements that forwarding and conforms to UNUserNotificationCenterDelegate, so your host app only needs to assign it in AppDelegate.swift:

import UserNotifications // add this import

// ...inside application(_:didFinishLaunchingWithOptions:), before `return super...`:
if #available(iOS 10.0, *) {
  UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
macOS Setup

Unlike iOS, FlutterAppDelegate on macOS does not conform to UNUserNotificationCenterDelegate. You must explicitly declare compliance and implement the callback in macos/Runner/AppDelegate.swift:

import UserNotifications // add this import

@main
class AppDelegate: FlutterAppDelegate, UNUserNotificationCenterDelegate {
  override func applicationDidFinishLaunching(_ notification: Notification) {
    UNUserNotificationCenter.current().delegate = self
    super.applicationDidFinishLaunching(notification)
  }

  // Handle foreground notifications on macOS
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  ) {
    completionHandler([.alert, .sound])
  }
}

See example/ios/Runner/AppDelegate.swift and example/macos/Runner/AppDelegate.swift for working references. Without this setup no foreground banner appears on iOS / macOS β€” the notification is still delivered silently to Notification Center, and tapping it still works.

If permission is denied or the platform isn't supported, the notifier degrades silently to a no-op β€” it never crashes your app.

Log messages

inspector.log('User signed in', level: LogLevel.info);

inspector.log(
  'Payment failed',
  level: LogLevel.error,
  data: {'orderId': 'A123', 'amount': 4200},
  stackTrace: stackTrace.toString(),
);

Available levels: verbose, debug, info, warning, error.

Read the merged timeline

The Console tab already interleaves logs, network, navigation, and database events on a single timeline (newest first), with a filter chip per source. You can also read that same merged view programmatically:

// All sources, newest first.
final entries = inspector.mergedTimeline();

// Filter to specific sources only.
final networkAndLogs = inspector.mergedTimeline(
  sources: {TimelineSource.network, TimelineSource.log},
);

for (final entry in entries) {
  // displayTime is a shared HH:mm:ss.mmm helper on every TimestampedEntry.
  print('${entry.displayTime}  $entry');
}

mergedTimeline returns List<TimestampedEntry> sorted by timestamp descending. Available sources: TimelineSource.log, .network, .nav, .db (defaults to all). The entries are the live buffer objects, so a pending network call that later completes is reflected on the next read.

Uncaught error capture (opt-in)

By default you have to log errors yourself. Enable uncaught error capture to have the inspector automatically turn uncaught errors into error-level Console logs β€” no manual try/catch needed.

It is disabled by default so the package never touches your error handling unless you ask. Enable it on the constructor:

final inspector = FlutterInspector(captureUncaughtErrors: true);

This wires three standard Flutter hooks β€” FlutterError.onError (build/layout/paint errors), PlatformDispatcher.instance.onError (uncaught async errors, including unawaited Future errors), and ErrorWidget.builder (which widget failed to build). Together they cover framework, asynchronous and build-time errors without wrapping runApp in a custom zone, so there is no Zone mismatch to manage.

Errors are never swallowed. Every hook chains/wraps your existing handler rather than replacing it: the inspector records the error and then forwards it downstream (your handler, or Flutter's default presentation β€” debug red screen / release grey screen unchanged). The capture is purely additive.

Captured errors appear as red logs in the Console tab. Tap any log that carries a stack trace or structured data to open a detail view with a copyable stack trace and the structured payload, plus copy/share actions.

Track navigation

Nothing to do here β€” routes are tracked automatically once you register inspector.navigatorObserver in navigatorObservers (see Initialize). Pushes, pops, and replacements all show up in the Navigator tab.

The Navigator tab offers two views, toggled via chips at the top:

  • Event History: the raw log of push/pop/replace/remove events (the original behavior).
  • Active Stack: the current route stack, derived live from the event history and rendered top-first as vertical cards β€” the topmost card (the current screen) is highlighted. Shows "Empty stack history" when no routes have been recorded yet.

Track database operations

Record database operations so you can review them in the dashboard.

inspector.database(
  DatabaseOperation.update,
  'users',
  affectedRows: 1,
  data: {'query': 'UPDATE users SET name = ? WHERE id = ?'},
);

Available operations: insert, update, delete, query.

Browse database tables

You can browse tables and rows directly from the Database tab. By default, operations logged via inspector.database(...) are grouped into virtual tables.

To browse real databases (e.g. SQLite, ObjectBox), implement DatabaseBrowserSource and register it.

SQLite Adapter Example

Here is a complete, copy-pasteable implementation of DatabaseBrowserSource for sqflite:

import 'package:flutter_inspector_kit/flutter_inspector_kit.dart';
import 'package:sqflite/sqflite.dart';

class SqfliteBrowserSource implements DatabaseBrowserSource {
  SqfliteBrowserSource(this._db, {this.name = 'SQLite database'});

  final Database _db;

  @override
  final String name;

  @override
  Future<List<DatabaseTableInfo>> listTables() async {
    final List<Map<String, Object?>> tables = await _db.rawQuery(
      "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
    );

    final List<DatabaseTableInfo> result = [];
    for (final table in tables) {
      final name = table['name'] as String;
      final countResult = await _db.rawQuery('SELECT COUNT(*) as count FROM "$name"');
      final rowCount = Sqflite.firstIntValue(countResult);
      result.add(DatabaseTableInfo(name: name, rowCount: rowCount));
    }
    return result;
  }

  @override
  Future<DatabaseTablePage> fetchRows(
    String tableName, {
    int limit = 200,
    int offset = 0,
  }) async {
    final countResult = await _db.rawQuery('SELECT COUNT(*) as count FROM "$tableName"');
    final totalRows = Sqflite.firstIntValue(countResult) ?? 0;

    final List<Map<String, Object?>> queryResult = await _db.rawQuery(
      'SELECT * FROM "$tableName" LIMIT ? OFFSET ?',
      [limit, offset],
    );

    if (queryResult.isEmpty) {
      final tableInfo = await _db.rawQuery('PRAGMA table_info("$tableName")');
      final columns = tableInfo.map((info) => info['name'] as String).toList();
      return DatabaseTablePage(
        columns: columns,
        rows: const [],
        totalRows: totalRows,
      );
    }

    final columns = queryResult.first.keys.toList();
    final rows = queryResult.map((map) {
      return columns.map((col) => map[col]).toList();
    }).toList();

    return DatabaseTablePage(
      columns: columns,
      rows: rows,
      totalRows: totalRows,
    );
  }
}

ObjectBox Adapter Example

For ObjectBox, since Box/Entity represents a table and reflection is not available at runtime to convert entities to map, you can register entities manually:

import 'package:flutter_inspector_kit/flutter_inspector_kit.dart';
import 'package:objectbox/objectbox.dart';

class ObjectBoxEntityInfo<T> {
  ObjectBoxEntityInfo({
    required this.name,
    required this.box,
    required this.toMap,
  });

  final String name;
  final Box<T> box;
  final Map<String, dynamic> Function(T) toMap;
}

class ObjectBoxBrowserSource implements DatabaseBrowserSource {
  ObjectBoxBrowserSource({
    required this.entities,
    this.name = 'ObjectBox database',
  });

  final List<ObjectBoxEntityInfo> entities;

  @override
  final String name;

  @override
  Future<List<DatabaseTableInfo>> listTables() async {
    return entities.map((e) {
      return DatabaseTableInfo(
        name: e.name,
        rowCount: e.box.count(),
      );
    }).toList();
  }

  @override
  Future<DatabaseTablePage> fetchRows(
    String tableName, {
    int limit = 200,
    int offset = 0,
  }) async {
    final entityInfo = entities.firstWhere((e) => e.name == tableName);
    final totalRows = entityInfo.box.count();

    // Query with offset and limit
    final query = entityInfo.box.query().build();
    query.limit = limit;
    query.offset = offset;
    final items = query.find();
    query.close();

    if (items.isEmpty) {
      return DatabaseTablePage(
        columns: [],
        rows: const [],
        totalRows: totalRows,
      );
    }

    final maps = items.map((item) => entityInfo.toMap(item)).toList();
    final columns = maps.first.keys.toList();
    final rows = maps.map((map) => columns.map((col) => map[col]).toList()).toList();

    return DatabaseTablePage(
      columns: columns,
      rows: rows,
      totalRows: totalRows,
    );
  }
}

Registration

You can register these sources when initializing FlutterInspector or dynamically at runtime:

// At initialization
final inspector = FlutterInspector(
  databaseSources: [SqfliteBrowserSource(db)],
);

// Or dynamically
inspector.registerDatabaseSource(SqfliteBrowserSource(db));

Export a diagnostic report

Open the dashboard and tap the share icon in the app bar. Pick which sources to include, a time window (last 5m / last 1h / all), and optionally "errors & warnings only", then hit Share report β€” a Markdown report goes straight to the system share sheet. Nothing is written to disk.

The report leads with a single chronological Timeline that interleaves the log, network, navigation, and database entries you selected by timestamp (newest first), so cross-layer causality reads at a glance; the per-source Network / Navigation / Database detail sections follow below it. The "errors & warnings only" toggle filters this whole Timeline down to error signals (log errors/warnings plus failed network calls), leaving those detail sections untouched.

The report inherits your redactSensitiveData setting, and its header states Redaction: enabled / disabled so whoever receives it knows what was masked.

Populating the device / app header (optional)

This package depends on no device-info plugin (and never touches dart:io, so it stays WASM-compatible). Without a source, the header degrades to N/A and the report is still produced in full.

To fill it in, add package_info_plus and device_info_plus to your own app, then implement DiagnosticInfoSource:

import 'dart:io' show Platform;

import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_inspector_kit/flutter_inspector_kit.dart';
import 'package:package_info_plus/package_info_plus.dart';

// Note: This example is mobile-only due to its dart:io Platform usage.
// It will not compile on Web/WASM.
class AppDiagnosticInfoSource implements DiagnosticInfoSource {
  @override
  Future<DiagnosticInfo> collect() async {
    final pkg = await PackageInfo.fromPlatform();
    final deviceInfo = DeviceInfoPlugin();

    String? deviceModel;
    String? osVersion;

    if (Platform.isIOS) {
      final ios = await deviceInfo.iosInfo;
      deviceModel = ios.utsname.machine;
      osVersion = 'iOS ${ios.systemVersion}';
    } else if (Platform.isAndroid) {
      final android = await deviceInfo.androidInfo;
      deviceModel = '${android.manufacturer} ${android.model}';
      osVersion = 'Android ${android.version.release}';
    }

    return DiagnosticInfo(
      appVersion: '${pkg.version}+${pkg.buildNumber}',
      deviceModel: deviceModel,
      osVersion: osVersion,
    );
  }
}

Then pass it in:

final inspector = FlutterInspector(
  diagnosticInfoSource: AppDiagnosticInfoSource(),
);

πŸ•ΉοΈ Example

A complete, runnable integration lives in the example/ directory:

cd example
flutter run

πŸ“„ License

This project is licensed under the terms described in the LICENSE file.