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

A compact, resizable in-app floating log viewer for Flutter applications.

example/lib/main.dart

import 'dart:async';

import 'package:file_saver/file_saver.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_log_viewer/flutter_log_viewer.dart';
import 'package:open_filex/open_filex.dart';

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

final messengerKey = GlobalKey<ScaffoldMessengerState>();
final navigatorKey = GlobalKey<NavigatorState>();
bool _isExportLoadingVisible = false;

final logController = LogViewerController(
  label: 'Example Logs',
  showGuideOnInit: true,
  exportHandler: LogViewerExportHandler(
    exportStart: _showExportLoading,
    exportEnd: _saveExportedLog,
  ),
  toast: (_, message) => _showMessage(message),
);

void _showExportLoading(BuildContext context, LogViewerController controller) {
  final dialogContext = navigatorKey.currentContext ?? context;
  _isExportLoadingVisible = true;
  unawaited(
    showDialog<void>(
      context: dialogContext,
      barrierDismissible: false,
      builder: (context) => const Center(
        child: Card(
          child: Padding(
            padding: EdgeInsets.all(18),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
                SizedBox(width: 14),
                Text('Exporting logs...'),
              ],
            ),
          ),
        ),
      ),
    ).whenComplete(() => _isExportLoadingVisible = false),
  );
}

Future<void> _saveExportedLog(
  BuildContext context,
  LogViewerController controller,
  LogViewerExportFile file,
) async {
  final dialogContext = navigatorKey.currentContext ?? context;
  if (_isExportLoadingVisible) {
    Navigator.of(dialogContext, rootNavigator: true).pop();
  }

  final extensionStart = file.fileName.lastIndexOf('.');
  final name = extensionStart == -1
      ? file.fileName
      : file.fileName.substring(0, extensionStart);
  final extension = extensionStart == -1
      ? 'log'
      : file.fileName.substring(extensionStart + 1);

  try {
    final savedPath = await _saveLogExport(
      file,
      name: name,
      extension: extension,
    );
    if (savedPath == null) return;
    if (_usesMobileFilePanel && savedPath.isNotEmpty) {
      final result = await OpenFilex.open(savedPath, type: file.mimeType);
      if (result.type != ResultType.done) {
        _showMessage(
          'Exported, but the file could not be opened: ${result.message}',
          long: true,
        );
        return;
      }
    }
    _showMessage('Exported ${file.fileName}');
  } catch (error) {
    _showMessage('Export failed: $error', long: true);
  }
}

bool get _usesDesktopSaveDialog =>
    !kIsWeb &&
    switch (defaultTargetPlatform) {
      TargetPlatform.linux ||
      TargetPlatform.macOS ||
      TargetPlatform.windows => true,
      _ => false,
    };

bool get _usesMobileFilePanel =>
    !kIsWeb &&
    switch (defaultTargetPlatform) {
      TargetPlatform.android || TargetPlatform.iOS => true,
      _ => false,
    };

Future<String?> _saveLogExport(
  LogViewerExportFile file, {
  required String name,
  required String extension,
}) async {
  if (_usesDesktopSaveDialog) {
    final location = await getSaveLocation(
      suggestedName: defaultTargetPlatform == TargetPlatform.macOS
          ? name
          : file.fileName,
      acceptedTypeGroups: const <XTypeGroup>[
        XTypeGroup(label: 'Log file', extensions: <String>['log']),
      ],
    );
    if (location == null) return null;
    await XFile.fromData(
      file.bytes,
      mimeType: file.mimeType,
      name: file.fileName,
    ).saveTo(location.path);
    return location.path;
  }

  return FileSaver.instance.saveFile(
    name: name,
    bytes: file.bytes,
    fileExtension: extension,
    mimeType: MimeType.custom,
    customMimeType: file.mimeType,
  );
}

void _showMessage(String message, {bool long = false}) {
  messengerKey.currentState
    ?..hideCurrentSnackBar()
    ..showSnackBar(
      SnackBar(
        content: Text(message),
        behavior: SnackBarBehavior.floating,
        duration: Duration(seconds: long ? 4 : 2),
      ),
    );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      scaffoldMessengerKey: messengerKey,
      title: 'Flutter Log Viewer',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      ),
      builder: (context, child) => LogViewerOverlay(
        controller: logController,
        child: child ?? const SizedBox.shrink(),
      ),
      home: const ExampleHomePage(),
    );
  }
}

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

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  Timer? _timer;
  int _sequence = 0;

  bool get _streaming => _timer?.isActive ?? false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _addInitialLogs());
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  void _addInitialLogs() {
    logController
      ..info('Application started', tag: 'Lifecycle')
      ..debug('Loaded local preferences in 18ms', tag: 'Storage')
      ..info('Signed in as demo@example.com', tag: 'Auth')
      ..warning('Using the staging API endpoint', tag: 'Config');
  }

  void _addLog(LogViewerLevel level) {
    _sequence++;
    logController.log(
      switch (level) {
        LogViewerLevel.verbose => 'Cache lookup completed for item $_sequence',
        LogViewerLevel.debug => 'State transition dispatched: event_$_sequence',
        LogViewerLevel.info => 'User action completed successfully',
        LogViewerLevel.warning => 'Request took ${420 + _sequence}ms',
        LogViewerLevel.error => 'Unable to decode response payload',
      },
      level: level,
      tag: switch (level) {
        LogViewerLevel.verbose => 'Cache',
        LogViewerLevel.debug => 'State',
        LogViewerLevel.info => 'UI',
        LogViewerLevel.warning => 'Network',
        LogViewerLevel.error => 'Parser',
      },
    );
  }

  void _addBurst() {
    for (final level in LogViewerLevel.values) {
      _addLog(level);
    }
  }

  void _toggleStream() {
    if (_streaming) {
      _timer?.cancel();
    } else {
      _timer = Timer.periodic(
        const Duration(milliseconds: 650),
        (_) => _addLog(LogViewerLevel.values[_sequence % 4]),
      );
    }
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter Log Viewer')),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 16, 16, 220),
        children: <Widget>[
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: <Widget>[
              FilledButton.tonalIcon(
                onPressed: logController.togglePanel,
                icon: const Icon(Icons.visibility_outlined, size: 18),
                label: const Text('Toggle viewer'),
              ),
              FilledButton.tonalIcon(
                onPressed: _toggleStream,
                icon: Icon(
                  _streaming ? Icons.stop : Icons.play_arrow,
                  size: 18,
                ),
                label: Text(_streaming ? 'Stop stream' : 'Start stream'),
              ),
              FilledButton.tonalIcon(
                onPressed: _addBurst,
                icon: const Icon(Icons.bolt, size: 18),
                label: const Text('Add burst'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          const Divider(),
          const SizedBox(height: 12),
          Text('Log levels', style: Theme.of(context).textTheme.titleSmall),
          const SizedBox(height: 10),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: <Widget>[
              for (final level in LogViewerLevel.values)
                OutlinedButton(
                  onPressed: () => _addLog(level),
                  child: Text(level.name.toUpperCase()),
                ),
            ],
          ),
          const SizedBox(height: 22),
          Text('Viewer state', style: Theme.of(context).textTheme.titleSmall),
          const SizedBox(height: 8),
          ListenableBuilder(
            listenable: logController,
            builder: (context, _) => Column(
              children: <Widget>[
                SwitchListTile.adaptive(
                  contentPadding: EdgeInsets.zero,
                  title: const Text('Record logs'),
                  subtitle: Text(
                    logController.isRecording
                        ? 'New entries are being accepted.'
                        : 'Calls are ignored until recording resumes.',
                  ),
                  value: logController.isRecording,
                  onChanged: logController.setRecording,
                ),
                ListTile(
                  contentPadding: EdgeInsets.zero,
                  title: const Text('Cached entries'),
                  trailing: Text('${logController.entries.length} / 1000'),
                ),
              ],
            ),
          ),
          const SizedBox(height: 8),
          DecoratedBox(
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(7),
            ),
            child: const Padding(
              padding: EdgeInsets.all(14),
              child: Text(
                'Try tapping a row, long-pressing and dragging across rows, '
                'moving the toolbar, and resizing from the lower-right corner.',
                style: TextStyle(fontSize: 12, height: 1.4),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
26
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A compact, resizable in-app floating log viewer for Flutter applications.

Repository (GitHub)
View/report issues

Topics

#logging #debugging #overlay #inspector #flutter

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_log_viewer