logging_to_logcat 1.1.0
logging_to_logcat: ^1.1.0 copied to clipboard
Adds activateLogcat() method to logging's Logger class which results in everything that's logged to Logger being displayed by Android's logcat.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:logging_to_logcat/logging_to_logcat.dart';
const String fineMessage = 'This is a fine message';
const String configMessage = 'This is a config message';
const String infoMessage = 'This is an info message';
const String warningMessage = 'This is a warning message';
const String errorMessage = 'This is an error message';
void initLogging() {
Logger.root
..level = Level.ALL
..activateLogcat();
}
void addLog() {
final log = Logger('ExampleLogger');
log
..fine(fineMessage)
..config(configMessage)
..info(infoMessage)
..warning(warningMessage)
..severe(errorMessage);
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static const int _maximumRecords = 50;
final List<LogRecord> _records = <LogRecord>[];
late final StreamSubscription<LogRecord> _logSubscription;
@override
void initState() {
super.initState();
initLogging();
_logSubscription = Logger.root.onRecord.listen((LogRecord record) {
if (!mounted) {
return;
}
setState(() {
_records.add(record);
if (_records.length > _maximumRecords) {
_records.removeAt(0);
}
});
});
}
@override
void dispose() {
_logSubscription.cancel();
super.dispose();
}
void _clearRecords() {
setState(_records.clear);
}
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF4F46E5),
);
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: colorScheme,
useMaterial3: true,
scaffoldBackgroundColor: colorScheme.surface,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Logcat Relay'),
actions: <Widget>[
IconButton(
tooltip: 'Clear entries',
onPressed: _records.isEmpty ? null : _clearRecords,
icon: const Icon(Icons.delete_outline),
),
],
),
body: Column(
children: <Widget>[
_StatusCard(recordCount: _records.length),
Expanded(
child: _records.isEmpty
? const _EmptyLogState()
: _LogList(records: _records),
),
_ActionBar(onPressed: addLog),
],
),
),
);
}
}
class _StatusCard extends StatelessWidget {
const _StatusCard({required this.recordCount});
final int recordCount;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
margin: const EdgeInsets.fromLTRB(16, 12, 16, 8),
color: colorScheme.secondaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: <Widget>[
CircleAvatar(
backgroundColor: colorScheme.secondary,
foregroundColor: colorScheme.onSecondary,
child: const Icon(Icons.terminal_rounded),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Android Logcat',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 2),
Text(
'Forwarding Flutter logs in real time',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
_ActiveBadge(color: colorScheme.primary),
const SizedBox(height: 6),
Text(
'$recordCount entries',
style: Theme.of(context).textTheme.labelMedium,
),
],
),
],
),
),
);
}
}
class _ActiveBadge extends StatelessWidget {
const _ActiveBadge({required this.color});
final Color color;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: ShapeDecoration(
color: color.withValues(alpha: 0.14),
shape: const StadiumBorder(),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.circle, color: color, size: 8),
const SizedBox(width: 5),
Text('ACTIVE', style: Theme.of(context).textTheme.labelSmall),
],
),
),
);
}
}
class _EmptyLogState extends StatelessWidget {
const _EmptyLogState();
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.receipt_long_outlined,
size: 56,
color: colorScheme.outline,
),
const SizedBox(height: 16),
Text(
'No entries yet',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 6),
Text(
'Send the sample messages to see the records that are also written to Android Logcat.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}
}
class _LogList extends StatelessWidget {
const _LogList({required this.records});
final List<LogRecord> records;
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
itemCount: records.length + 1,
separatorBuilder: (_, index) =>
index == 0 ? const SizedBox(height: 4) : const SizedBox(height: 8),
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return Text(
'RECENT ENTRIES',
style: Theme.of(context).textTheme.labelMedium,
);
}
return _LogRecordCard(record: records[index - 1]);
},
);
}
}
class _LogRecordCard extends StatelessWidget {
const _LogRecordCard({required this.record});
final LogRecord record;
@override
Widget build(BuildContext context) {
final color = _levelColor(record.level, Theme.of(context).colorScheme);
return Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(_levelIcon(record.level), color: color),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
record.message,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 8),
Row(
children: <Widget>[
_LevelChip(level: record.level, color: color),
const SizedBox(width: 8),
Expanded(
child: Text(
'${_formatTime(record.time)} · ${record.loggerName}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelMedium,
),
),
],
),
],
),
),
],
),
),
);
}
}
class _LevelChip extends StatelessWidget {
const _LevelChip({required this.level, required this.color});
final Level level;
final Color color;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: ShapeDecoration(
color: color.withValues(alpha: 0.12),
shape: const StadiumBorder(),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
level.name,
style: Theme.of(context).textTheme.labelSmall?.copyWith(color: color),
),
),
);
}
}
class _ActionBar extends StatelessWidget {
const _ActionBar({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.play_arrow_rounded),
label: const Text('Emit sample logs'),
),
),
);
}
}
Color _levelColor(Level level, ColorScheme colorScheme) {
if (level == Level.SHOUT || level == Level.SEVERE) {
return colorScheme.error;
}
if (level == Level.WARNING) {
return const Color(0xFFB45309);
}
if (level == Level.INFO) {
return colorScheme.primary;
}
if (level == Level.CONFIG) {
return const Color(0xFF7C3AED);
}
return const Color(0xFF0F766E);
}
IconData _levelIcon(Level level) {
if (level == Level.SHOUT || level == Level.SEVERE) {
return Icons.error_outline_rounded;
}
if (level == Level.WARNING) {
return Icons.warning_amber_rounded;
}
if (level == Level.INFO) {
return Icons.info_outline_rounded;
}
return Icons.terminal_rounded;
}
String _formatTime(DateTime time) {
String twoDigits(int value) => value.toString().padLeft(2, '0');
String threeDigits(int value) => value.toString().padLeft(3, '0');
return '${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}.${threeDigits(time.millisecond)}';
}