đ Flutter Inspector
In-app, multi-inspector debugging overlay for Flutter apps â logs, network, navigation, and database, all behind one unified API.
đĻ Features
- đĒĩ Console: capture logs across five severity levels, with optional structured data and stack traces
- đĄ Network: intercept HTTP traffic via Dio, inspect structured request/response details, search/filter, share as cURL
- đ§ Navigator: track route pushes, pops, and replacements automatically
- đī¸ Database: record insert / update / delete / query operations with affected-row counts and payloads
- đ Magical tap & floating button: open the dashboard with a hidden multi-tap gesture or a draggable in-app FAB
- đ Live notification (opt-in): a system notification that summarises the latest API call and the running total
đą Screenshots
| Home | Console | Network |
|---|---|---|
![]() |
![]() |
![]() |
| Network Detail | Navigator | Database |
|---|---|---|
![]() |
![]() |
![]() |
đĒ Usage
Add to pubspec.yaml
dependencies:
flutter_inspector_kit: ^0.2.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().
Log network requests
With Dio
Add the interceptor to your Dio instance and every request/response is captured automatically.
final dio = Dio();
dio.interceptors.add(FlutterInspectorDioInterceptor(inspector));
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. - 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.
- Sharing: copy the call as a runnable
cURLcommand, copy the full details as text, or open the system share sheet (native viashare_plus, web via the browser Web Share API â falls back to the clipboard when unavailable).
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 silent foreground banner when enabled.
- 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.
On iOS / macOS, the user is prompted for notification permission when the inspector initialises.
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.
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.
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));
đšī¸ 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.





