Skip to main content

flutter_inspector_kit 2.9.0 copy "flutter_inspector_kit: ^2.9.0" to clipboard
flutter_inspector_kit: ^2.9.0 copied to clipboard

A multi-inspector tool integration for Flutter, bundling debugging and inspection utilities behind a single unified API.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_inspector_kit/flutter_inspector_kit.dart';
import 'package:logger/logger.dart' show Logger;
import 'package:logging/logging.dart' as logging;
import 'package:talker/talker.dart' show Talker;

import 'bridges/logger_bridge.dart';
import 'bridges/logging_bridge.dart';
import 'bridges/talker_bridge.dart';
import 'demos/inappwebview_demo.dart';
import 'demos/log_bridges_demo.dart';
import 'demos/network_demo.dart';
import 'demos/objectbox_demo.dart';
import 'demos/shared_prefs_demo.dart';
import 'demos/sqlite_demo.dart';
import 'demos/webview_demo.dart';

// Enable the live system notification summarising network calls (opt-in).
// On Android this requires a notification icon + (Android 13+) the
// POST_NOTIFICATIONS permission; on iOS/macOS the user is prompted on init.
late final FlutterInspector inspector;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late final Talker talker;

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  inspector = FlutterInspector(
    showNetworkNotification: true,
    navigatorKey: navigatorKey,
    // Capture uncaught errors into the Console as LogLevel.error logs (opt-in).
    // This wires the three standard hooks — FlutterError.onError,
    // PlatformDispatcher.instance.onError and ErrorWidget.builder — which
    // together cover framework, asynchronous and build-time errors. No zone is
    // involved, so there is no Zone mismatch to worry about.
    captureUncaughtErrors: true,
    // Also raise a throttled system notification for uncaught errors (opt-in).
    // Requires captureUncaughtErrors above: without the hooks there is no
    // event to notify about. Uses its own notification id, channel and
    // throttler, so a crash alert never replaces the network summary — but a
    // burst of errors re-alerts at most once per throttle window. Native
    // platforms only; the web build is a no-op.
    showCrashNotification: true,
    // Record app foreground/background transitions as info logs (opt-in).
    // Each entry names the current top page (e.g. "resumed · HomePage"), so
    // the nearest lifecycle entry above a crash tells you both whether the app
    // was in the foreground and which page it was on.
    captureLifecycleEvents: true,
    // Capture frozen frame rendering events (>= 700ms) as warning logs (opt-in).
    // Aligned with Android Vitals frozen frames to correlate UI freezes with
    // preceding network, database or navigation events.
    capturePerformance: true,
  );
  // Bridge existing loggers into the Console timeline (README: "Bridge
  // existing loggers"). Every hook is synchronous, so a bridged record keeps
  // its place among network / navigation / database events. Wire them once.
  Logger.addOutputListener((event) => forwardLoggerEvent(inspector, event));
  talker = Talker(observer: InspectorTalkerObserver(inspector));
  // DEMO ONLY, do not copy: logging's root defaults to INFO, which drops
  // CONFIG and below; lowered so every demo level reaches the timeline. The
  // bridge never touches the level, so your own threshold still applies.
  logging.Logger.root.level = logging.Level.ALL;
  logging.Logger.root.onRecord.listen((r) => forwardLogRecord(inspector, r));
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      title: 'Flutter Inspector Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      navigatorObservers: [inspector.navigatorObserver],
      builder: (context, child) {
        return FlutterInspectorMagicalTap(
          onTap: () {
            final context = navigatorKey.currentContext;

            if (context == null) return;

            inspector.openDashboard();
          },
          child: child ?? const SizedBox.shrink(),
        );
      },
      home: const MyHomePage(title: 'Flutter Inspector Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  late final NetworkDemo _networkDemo;
  late final SqliteDemo _sqliteDemo;
  late final ObjectBoxDemo _objectBoxDemo;
  late final SharedPrefsDemo _sharedPrefsDemo;
  late final WebViewDemo _webViewDemo;
  late final InAppWebViewDemo _inAppWebViewDemo;
  late final LogBridgesDemo _logBridgesDemo;

  @override
  void initState() {
    super.initState();
    _networkDemo = NetworkDemo(inspector);
    _sqliteDemo = SqliteDemo(inspector);
    _objectBoxDemo = ObjectBoxDemo(inspector);
    _sharedPrefsDemo = SharedPrefsDemo(inspector);
    _webViewDemo = WebViewDemo(inspector);
    _inAppWebViewDemo = InAppWebViewDemo(inspector);
    _logBridgesDemo = LogBridgesDemo(talker);

    // Show FAB after frame builds
    WidgetsBinding.instance.addPostFrameCallback((_) {
      inspector.attach(context: context);
    });
  }

  @override
  void dispose() {
    _sqliteDemo.dispose();
    _objectBoxDemo.dispose();
    super.dispose();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    inspector.log('Counter incremented to $_counter', level: LogLevel.info);
    inspector.database(
      DatabaseOperation.update,
      'counters',
      affectedRows: 1,
      data: {'query': 'UPDATE counters SET val = $_counter'},
    );
  }

  /// Runs a demo's seed step and surfaces its status message, if any.
  Future<void> _runSeed(Future<String?> Function() seed) async {
    final message = await seed();
    if (message != null && mounted) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(message)));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              const Text('You have pushed the button this many times:'),
              Text(
                '$_counter',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: _networkDemo.makeRequest,
                child: const Text('Make Network Request'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: _networkDemo.makeSlowRequest,
                child: const Text('Make Slow Network Request (3s)'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () => _runSeed(_sqliteDemo.seed),
                child: const Text('Seed SQLite Demo'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () => _runSeed(_objectBoxDemo.seed),
                child: const Text('Seed ObjectBox Demo'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () => _runSeed(_sharedPrefsDemo.seed),
                child: const Text('Seed SharedPreferences Demo'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () => _webViewDemo.open(context),
                child: const Text('Open WebView Demo'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () => _inAppWebViewDemo.open(context),
                child: const Text('Open InAppWebView Demo'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  // Exercises the Navigator tab's Params (name query) vs Args
                  // (arguments) split, plus <complex> for collections,
                  // 100-char truncation, and •••• for sensitive keys.
                  Navigator.of(context).push(
                    MaterialPageRoute(
                      settings: RouteSettings(
                        name: '/second?id=99&source=push&token=abc123',
                        arguments: {
                          'orderId': 1001,
                          'isVip': true,
                          'tab': 'detail',
                          'password': 'hunter2',
                          'cart': [1, 2, 3],
                          'note': 'x' * 150,
                        },
                      ),
                      builder: (_) => Scaffold(
                        appBar: AppBar(title: const Text('Second Page')),
                        body: const Center(child: Text('Second Page')),
                      ),
                    ),
                  );
                },
                child: const Text('Push New Route'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                // Fires the same observer broadcast the OS triggers on real
                // memory pressure (Android onTrimMemory, iOS
                // didReceiveMemoryWarning). With captureLifecycleEvents
                // enabled it lands on the timeline as a warning entry.
                onPressed: () => WidgetsBinding.instance.handleMemoryPressure(),
                child: const Text('Simulate Memory Pressure'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                // Simulates a frozen frame (~900ms busy wait on the UI thread).
                // With capturePerformance enabled, this single-frame stall exceeds
                // the 700ms threshold and is recorded as a warning log with
                // duration breakdowns.
                onPressed: () {
                  final sw = Stopwatch()..start();
                  while (sw.elapsedMilliseconds < 900) {
                    // busy-wait to freeze the UI thread
                  }
                },
                child: const Text('Simulate Frozen Frame (900ms)'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                // Throws an uncaught async error. With captureUncaughtErrors
                // enabled it is caught by PlatformDispatcher.instance.onError and
                // surfaces as a red error log in the Console tab — tap it to
                // expand the stack trace.
                onPressed: () => Future<void>.error(
                  StateError('Demo: uncaught async error'),
                  StackTrace.current,
                ),
                child: const Text('Throw Uncaught Error'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(
                    MaterialPageRoute(
                      builder: (_) => const _BrokenWidgetPage(),
                    ),
                  );
                },
                child: const Text('Trigger Widget Build Error'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                // Triggers an intentional layout overflow. With captureUncaughtErrors
                // enabled, FlutterError.onError captures the RenderFlex error,
                // parses the overflow pixels and direction, and displays a
                // RenderFlex badge in the Console tab.
                onPressed: () {
                  Navigator.of(context).push(
                    MaterialPageRoute(
                      builder: (_) => const _RenderFlexOverflowPage(),
                    ),
                  );
                },
                child: const Text('Trigger RenderFlex Overflow'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                // One record per level through logger, talker and logging,
                // plus one stack-traced error each, all via the bridges wired
                // in main(). Check colours, red rows and ⚡ Errors only.
                onPressed: _logBridgesDemo.emitAll,
                child: const Text('Emit Bridged Logs'),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

class _BrokenWidgetPage extends StatelessWidget {
  const _BrokenWidgetPage();

  @override
  Widget build(BuildContext context) {
    // Intentionally throw inside build method to trigger FlutterError.onError & ErrorWidget.builder
    throw FlutterError('Demo: Widget build error triggered inside build()');
  }
}

class _RenderFlexOverflowPage extends StatelessWidget {
  const _RenderFlexOverflowPage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('RenderFlex Overflow Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'This Row intentionally overflows horizontally by at least 200 pixels to trigger a RenderFlex error.\n'
                'Check the Inspector Console tab for the RenderFlex badge!',
                textAlign: TextAlign.center,
              ),
            ),
            const SizedBox(height: 20),
            // Fixed 400px width (< 600px of children) so the overflow also
            // triggers on wide screens.
            SizedBox(
              width: 400,
              child: Row(
                children: const [
                  SizedBox(
                    width: 300,
                    height: 60,
                    child: ColoredBox(
                      color: Colors.blue,
                      child: Center(
                        child: Text(
                          'Box 1 (300px)',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                    ),
                  ),
                  SizedBox(
                    width: 300,
                    height: 60,
                    child: ColoredBox(
                      color: Colors.red,
                      child: Center(
                        child: Text(
                          'Box 2 (300px)',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
42
likes
150
points
1.1k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A multi-inspector tool integration for Flutter, bundling debugging and inspection utilities behind a single unified API.

Repository (GitHub)
View/report issues

Topics

#debugging #developer-tools #network #logging

License

MIT (license)

Dependencies

dio, flutter, flutter_local_notifications, share_plus, web

More

Packages that depend on flutter_inspector_kit