Skip to main content

debug_bubble 0.1.0 copy "debug_bubble: ^0.1.0" to clipboard
debug_bubble: ^0.1.0 copied to clipboard

A reusable Flutter debugging toolkit with an in-app floating log viewer and error dashboard.

example/lib/main.dart

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

void main() {
  // Initialize toolkit before running the application.
  // Logs emitted before init are silently dropped (L-4, D-24).
  DebugLog.init(const DebugBubbleConfig(captureUncaughtErrors: true));

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'debug_bubble Example',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      builder: (context, child) =>
          DebugBubble(child: child ?? const SizedBox.shrink()),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  DebugBubbleController? _controller;

  @override
  void initState() {
    super.initState();
    _controller = DebugLog.controller;
    _controller?.addListener(_onControllerUpdate);
  }

  void _onControllerUpdate() {
    if (mounted) setState(() {});
  }

  @override
  void dispose() {
    _controller?.removeListener(_onControllerUpdate);
    super.dispose();
  }

  void _showExportDialog(BuildContext context, bool isJson) {
    final controller = _controller;
    if (controller == null) return;

    final content = isJson ? controller.exportJson() : controller.exportText();

    showDialog<void>(
      context: context,
      builder: (dialogContext) => AlertDialog(
        title: Text(isJson ? 'Exported JSON' : 'Exported Text'),
        content: SizedBox(
          width: double.maxFinite,
          height: 300,
          child: SingleChildScrollView(
            child: SelectableText(
              content.isEmpty ? '(No logs stored)' : content,
              style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
            ),
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(dialogContext).pop(),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final controller = _controller;
    final totalEntries = controller?.entryCount ?? 0;
    final errorCount = controller?.errorCount ?? 0;
    final droppedCount = controller?.droppedCount ?? 0;
    final isActive = controller?.isActive ?? false;
    final isDashboardOpen = controller?.isDashboardOpen ?? false;
    final recentEntries =
        controller?.entries.reversed.take(5).toList() ??
        const <DebugLogEntry>[];

    return Scaffold(
      appBar: AppBar(
        title: const Text('debug_bubble Example'),
        actions: [
          IconButton(
            tooltip: 'Clear logs',
            icon: const Icon(Icons.delete_outline),
            onPressed: () => DebugLog.controller?.clear(),
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Card(
              elevation: 0,
              color: Theme.of(context).colorScheme.surfaceContainerHighest,
              child: const Padding(
                padding: EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'In-App Debugging & Logging Demo',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(height: 6),
                    Text(
                      'This application demonstrates the debug_bubble package, '
                      'showcasing structured logging across 6 severity levels, '
                      'severity-colored console output in your IDE debug console, '
                      'payload redaction, ring buffer storage, and uncaught error capture.',
                      style: TextStyle(fontSize: 13),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            const Text(
              'Status & Metrics',
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: _MetricCard(
                    title: 'Total Logs',
                    value: '$totalEntries',
                    color: Colors.blue,
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: _MetricCard(
                    title: 'Errors',
                    value: '$errorCount',
                    color: Colors.red,
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: _MetricCard(
                    title: 'Dropped',
                    value: '$droppedCount',
                    color: Colors.orange,
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: _MetricCard(
                    title: 'Status',
                    value: isActive ? 'Active' : 'Inactive',
                    color: isActive ? Colors.green : Colors.grey,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 20),
            const Text(
              'Emit Log by Severity Level',
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                ActionChip(
                  avatar: const Icon(
                    Icons.bug_report,
                    size: 16,
                    color: Colors.grey,
                  ),
                  label: const Text('Debug'),
                  onPressed: () {
                    DebugLog.debug('Verbose debug event emitted', tag: 'UI');
                  },
                ),
                ActionChip(
                  avatar: const Icon(Icons.info, size: 16, color: Colors.blue),
                  label: const Text('Info'),
                  onPressed: () {
                    DebugLog.info('Application state changed', tag: 'App');
                  },
                ),
                ActionChip(
                  avatar: const Icon(
                    Icons.check_circle,
                    size: 16,
                    color: Colors.green,
                  ),
                  label: const Text('Success'),
                  onPressed: () {
                    DebugLog.success(
                      'Operation completed successfully',
                      tag: 'Sync',
                    );
                  },
                ),
                ActionChip(
                  avatar: const Icon(
                    Icons.warning,
                    size: 16,
                    color: Colors.amber,
                  ),
                  label: const Text('Warning'),
                  onPressed: () {
                    DebugLog.warning(
                      'Cache limit reaching threshold',
                      tag: 'Storage',
                    );
                  },
                ),
                ActionChip(
                  avatar: const Icon(Icons.error, size: 16, color: Colors.red),
                  label: const Text('Error'),
                  onPressed: () {
                    DebugLog.error(
                      'Failed to load user profile: 404',
                      tag: 'Network',
                      error: 'HttpException: 404 Not Found',
                    );
                  },
                ),
                ActionChip(
                  avatar: const Icon(
                    Icons.dangerous,
                    size: 16,
                    color: Color(0xFFAF52DE),
                  ),
                  label: const Text('Critical'),
                  onPressed: () {
                    DebugLog.critical(
                      'Critical unrecoverable failure encountered',
                      tag: 'Core',
                    );
                  },
                ),
              ],
            ),
            const SizedBox(height: 20),
            const Text(
              'Advanced Logging Actions',
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                FilledButton.tonalIcon(
                  icon: const Icon(Icons.data_object, size: 18),
                  label: const Text('Log with Metadata & JSON'),
                  onPressed: () {
                    DebugLog.info(
                      'User performed search query',
                      tag: 'Search',
                      metadata: {'userId': 'usr_9812', 'query': 'flutter'},
                      json: {'page': 1, 'pageSize': 20, 'results': 3},
                    );
                  },
                ),
                FilledButton.tonalIcon(
                  icon: const Icon(Icons.healing, size: 18),
                  label: const Text('Log Handled Exception'),
                  onPressed: () {
                    try {
                      throw const FormatException(
                        'Simulated parsing error in response body',
                      );
                    } catch (e, st) {
                      DebugLog.error(
                        'Handled parsing exception',
                        tag: 'Parser',
                        error: e,
                        stackTrace: st,
                      );
                    }
                  },
                ),
                FilledButton.tonalIcon(
                  icon: const Icon(Icons.report_problem, size: 18),
                  label: const Text('Trigger Uncaught Error'),
                  onPressed: () {
                    FlutterError.reportError(
                      FlutterErrorDetails(
                        exception: Exception(
                          'Simulated uncaught Flutter error',
                        ),
                        library: 'debug_bubble_example',
                        context: ErrorDescription(
                          'testing uncaught error hook',
                        ),
                      ),
                    );
                  },
                ),
              ],
            ),
            const SizedBox(height: 20),
            const Text(
              'Toolkit Controls & Export',
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                OutlinedButton.icon(
                  icon: Icon(
                    isDashboardOpen
                        ? Icons.visibility_off_outlined
                        : Icons.dashboard_outlined,
                    size: 18,
                  ),
                  label: Text(
                    isDashboardOpen ? 'Close Dashboard' : 'Open Dashboard',
                  ),
                  onPressed: () {
                    final c = DebugLog.controller;
                    if (c == null) return;
                    if (c.isDashboardOpen) {
                      c.closeDashboard();
                    } else {
                      c.openDashboard();
                    }
                  },
                ),
                OutlinedButton.icon(
                  icon: const Icon(Icons.text_snippet_outlined, size: 18),
                  label: const Text('Export Text'),
                  onPressed: () => _showExportDialog(context, false),
                ),
                OutlinedButton.icon(
                  icon: const Icon(Icons.code_outlined, size: 18),
                  label: const Text('Export JSON'),
                  onPressed: () => _showExportDialog(context, true),
                ),
                OutlinedButton.icon(
                  icon: const Icon(Icons.cleaning_services_outlined, size: 18),
                  label: const Text('Clear All'),
                  onPressed: () => DebugLog.controller?.clear(),
                ),
              ],
            ),
            const SizedBox(height: 20),
            const Text(
              'Recent Logs Preview (Latest 5)',
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            if (recentEntries.isEmpty)
              const Padding(
                padding: EdgeInsets.symmetric(vertical: 12.0),
                child: Text(
                  'No log entries yet. Tap a button above to generate logs.',
                  style: TextStyle(
                    color: Colors.grey,
                    fontStyle: FontStyle.italic,
                  ),
                ),
              )
            else
              ...recentEntries.map((entry) => _LogEntryTile(entry: entry)),
          ],
        ),
      ),
    );
  }
}

class _MetricCard extends StatelessWidget {
  const _MetricCard({
    required this.title,
    required this.value,
    required this.color,
  });

  final String title;
  final String value;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 0,
      color: color.withAlpha(20),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
        side: BorderSide(color: color.withAlpha(70)),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              value,
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
                color: color,
              ),
            ),
            const SizedBox(height: 2),
            Text(
              title,
              style: const TextStyle(fontSize: 11, color: Colors.black87),
              textAlign: TextAlign.center,
              overflow: TextOverflow.ellipsis,
            ),
          ],
        ),
      ),
    );
  }
}

class _LogEntryTile extends StatelessWidget {
  const _LogEntryTile({required this.entry});

  final DebugLogEntry entry;

  Color _levelColor(DebugLogLevel level) {
    switch (level) {
      case DebugLogLevel.debug:
        return Colors.grey;
      case DebugLogLevel.info:
        return Colors.blue;
      case DebugLogLevel.success:
        return Colors.green;
      case DebugLogLevel.warning:
        return Colors.orange;
      case DebugLogLevel.error:
        return Colors.red;
      case DebugLogLevel.critical:
        return const Color(0xFFAF52DE);
    }
  }

  @override
  Widget build(BuildContext context) {
    final color = _levelColor(entry.level);
    return Card(
      elevation: 0,
      margin: const EdgeInsets.symmetric(vertical: 4),
      color: Theme.of(context).colorScheme.surface,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(6),
        side: BorderSide(color: Colors.grey.withAlpha(50)),
      ),
      child: ListTile(
        dense: true,
        leading: Container(
          padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
          decoration: BoxDecoration(
            color: color.withAlpha(30),
            borderRadius: BorderRadius.circular(4),
          ),
          child: Text(
            entry.level.name.toUpperCase(),
            style: TextStyle(
              fontSize: 10,
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
        ),
        title: Text(entry.message, style: const TextStyle(fontSize: 13)),
        subtitle: entry.tag != null
            ? Text(
                'Tag: ${entry.tag}${entry.error != null ? ' | Error: ${entry.error!.message}' : ''}',
                style: const TextStyle(fontSize: 11),
              )
            : (entry.error != null
                  ? Text(
                      'Error: ${entry.error!.message}',
                      style: const TextStyle(fontSize: 11),
                    )
                  : null),
        trailing: Text(
          '#${entry.id}',
          style: const TextStyle(fontSize: 11, color: Colors.grey),
        ),
      ),
    );
  }
}
2
likes
160
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A reusable Flutter debugging toolkit with an in-app floating log viewer and error dashboard.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on debug_bubble