pd_log 0.5.1
pd_log: ^0.5.1 copied to clipboard
Cross-platform logging plugin for Flutter with buffered native file logging.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:pd_log/pd_log.dart';
/// 应用入口:启动示例应用。
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
/// 构造函数:创建应用根部件。
const MyApp({super.key});
@override
/// 创建应用的状态对象。
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _pdLogPlugin = PDLog();
bool _useConsole = true;
bool _useNative = false;
LogLevel _minLevel = LogLevel.verbose;
bool _showCaller = true;
bool _showTimestamp = true;
// 文件日志相关状态
bool _nativeFileEnabled = true;
final _flushIntervalCtrl = TextEditingController(text: '2000');
final _maxEntriesCtrl = TextEditingController(text: '100');
final _maxBytesCtrl = TextEditingController(text: '65536');
String? _logRootPath;
List<PDLogFile> _files = [];
int _totalSize = 0;
// 预览内容改为新页面展示
@override
/// 初始化:配置日志并拉取平台信息与初始文件列表。
void initState() {
super.initState();
// Configure logging defaults.
PDLog.configure(PDLogConfig(
defaultTag: 'Example 测试项目',
minLevel: _minLevel,
useNative: _useNative,
useConsole: _useConsole,
showCaller: _showCaller,
showTimestamp: _showTimestamp,
fileLoggingMinLevel: LogLevel.info,
nativeFileLoggingEnabled: _nativeFileEnabled,
nativeFileLoggingFlushIntervalMs:
int.tryParse(_flushIntervalCtrl.text) ?? 2000,
nativeFileLoggingMaxBufferEntries:
int.tryParse(_maxEntriesCtrl.text) ?? 100,
nativeFileLoggingMaxBufferBytes:
int.tryParse(_maxBytesCtrl.text) ?? 65536,
));
PDLog.i('App starting...');
initPlatformState();
_refreshRootPath();
_refreshFiles();
}
// Platform messages are asynchronous, so we initialize in an async method.
/// 异步获取平台版本信息并更新界面状态。
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _pdLogPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
/// 构建应用界面,包括日志配置与文件列表展示。
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Running on: $_platformVersion'),
const SizedBox(height: 12),
Row(
children: [
const Text('Use console logging'),
Switch(
value: _useConsole,
onChanged: (v) {
setState(() {
_useConsole = v;
PDLog.updateConfigure(
useConsole: _useConsole,
);
PDLog.d(
'Console logging: ${_useNative ? 'ON' : 'OFF'}',
);
});
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Text('Use native logging'),
Switch(
value: _useNative,
onChanged: (v) {
setState(() {
_useNative = v;
PDLog.updateConfigure(
useNative: _useNative,
);
PDLog.d(
'Native logging: ${_useNative ? 'ON' : 'OFF'}',
);
});
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Text('Show caller info'),
Switch(
value: _showCaller,
onChanged: (v) {
setState(() {
_showCaller = v;
PDLog.updateConfigure(
showCaller: _showCaller,
);
PDLog.d(
'Show caller: ${_showCaller ? 'ON' : 'OFF'}',
);
});
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Text('Show Timestamp'),
Switch(
value: _showTimestamp,
onChanged: (v) {
setState(() {
_showTimestamp = v;
PDLog.updateConfigure(
showTimestamp: _showTimestamp,
);
PDLog.d(
'Show Timestamp: ${_showTimestamp ? 'ON' : 'OFF'}',
);
});
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Text('Min level:'),
const SizedBox(width: 8),
DropdownButton<LogLevel>(
value: _minLevel,
onChanged: (level) {
if (level == null) return;
setState(() {
_minLevel = level;
PDLog.updateConfigure(
minLevel: _minLevel,
);
PDLog.d('Min level set to $level');
});
},
items: const [
DropdownMenuItem(
value: LogLevel.verbose,
child: Text('Verbose'),
),
DropdownMenuItem(
value: LogLevel.debug,
child: Text('Debug'),
),
DropdownMenuItem(
value: LogLevel.info,
child: Text('Info'),
),
DropdownMenuItem(
value: LogLevel.warn,
child: Text('Warn'),
),
DropdownMenuItem(
value: LogLevel.error,
child: Text('Error'),
),
],
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: () => PDLog.v('Verbose pressed'),
child: const Text('Verbose'),
),
ElevatedButton(
onPressed: () => PDLog.d('Debug pressed'),
child: const Text('Debug'),
),
ElevatedButton(
onPressed: () => PDLog.i('Info pressed'),
child: const Text('Info'),
),
ElevatedButton(
onPressed: () => PDLog.w('Warn pressed'),
child: const Text('Warn'),
),
ElevatedButton(
onPressed: () => PDLog.e('Error pressed'),
child: const Text('Error'),
),
ElevatedButton(
onPressed: () {
PDLog.out(
'自定义的错误警告输出: Customize pressed \n 测试换行后的文本样式',
tag: '自定义输出',
useConsole: false,
useNative: true,
toFile: true,
showTimestamp: false,
style: const LogStyleConfig(
foreground: 37, // 白色文本
background: 41, // 红色背景
styles: [1, 4, 5], // 粗体, 下划线, 闪烁
),
);
},
child: const Text('Customize'),
),
ElevatedButton(
onPressed: _logFromHelper,
child: const Text('Log from helper method'),
),
ElevatedButton(
onPressed: _runStressTest,
child: const Text('压力测试(1000 条)'),
),
],
),
const Divider(height: 32),
const Text(
'Native File Logging',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
const Text('Enable native file logging'),
Switch(
value: _nativeFileEnabled,
onChanged: (v) {
setState(() {
_nativeFileEnabled = v;
_applyConfig();
});
},
),
],
),
const SizedBox(height: 8),
Row(children: [
const SizedBox(
width: 140,
child: Text('Flush interval (ms)'),
),
Expanded(
child: TextField(
controller: _flushIntervalCtrl,
keyboardType: TextInputType.number,
),
),
]),
const SizedBox(height: 8),
Row(children: [
const SizedBox(
width: 140,
child: Text('Max buffer entries'),
),
Expanded(
child: TextField(
controller: _maxEntriesCtrl,
keyboardType: TextInputType.number,
),
),
]),
const SizedBox(height: 8),
Row(children: [
const SizedBox(
width: 140,
child: Text('Max buffer bytes'),
),
Expanded(
child: TextField(
controller: _maxBytesCtrl,
keyboardType: TextInputType.number,
),
),
]),
const SizedBox(height: 8),
Wrap(spacing: 8, children: [
ElevatedButton(
onPressed: _applyConfig,
child: const Text('应用配置'),
),
ElevatedButton(
onPressed: _flushNow,
child: const Text('立即刷新写盘'),
),
ElevatedButton(
onPressed: _refreshRootPath,
child: const Text('获取日志根路径'),
),
ElevatedButton(
onPressed: _refreshFiles,
child: const Text('列出日志文件'),
),
ElevatedButton(
onPressed: _deleteAllLogs,
child: const Text('删除全部日志'),
),
]),
const SizedBox(height: 8),
Text('日志根路径: ${_logRootPath ?? '(当前平台不支持或目录未创建)'}'),
const SizedBox(height: 8),
Text('当前总日志大小: $_totalSize 字节'),
const SizedBox(height: 8),
SizedBox(
height: 240,
child: Card(
child: ListView.builder(
itemCount: _files.length,
itemBuilder: (context, index) {
final f = _files[index];
return ListTile(
title: Text(f.path),
subtitle: Text(
'size=${f.sizeBytes}B modified=${DateTime.fromMillisecondsSinceEpoch(f.modifiedMs)}',
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteFile(f),
),
onTap: () => _readFileContent(f),
);
},
),
),
),
// 预览使用新页面
],
),
),
),
),
);
}
/// 示例:从辅助方法写入一条日志。
void _logFromHelper() async {
/// json美化打印
final Map arg = {
'name': 'pedro',
'age': 30,
'height': 183,
'no.': '9527',
'nickName': ['大头', '狗蛋'],
1: '下标',
'active': true,
'balance': 1234.56,
'createdAt': DateTime.now(),
'tags': {'flutter', 'dart', '日志'},
'addresses': [
{'type': 'home', 'city': '上海', 'zip': 200000},
{'type': 'work', 'city': '北京', 'zip': 100000},
],
'preferences': {
'notifications': {'email': true, 'sms': false},
'theme': {'mode': 'dark', 'accentColor': '#00FFFF'},
},
'metrics': {
'cpu': [0.12, 0.34, 0.56],
'mem': {'used': 2048, 'total': 8192},
'latencyMs': [12, 25, 8, 16],
},
'mixedList': [
null,
'文本',
42,
3.1415,
{
'nested': [
'a',
'b',
{'deep': 1}
]
},
],
'mapWithNonStringKeys': {1: '一', true: '真', 3.14: 'π'},
'emoji': '🚀🔥',
'longText': '这是一个多行文本\n用于测试换行\n以及格式化效果',
'url': Uri.parse('https://example.com/api?v=1'),
'bigInt': BigInt.from(1234567890123456789),
'mapDepthTest': {
'level1': {
'level2': {
'level3': {
'list': [
1,
2,
{'level4': 'ok'}
],
},
},
},
},
};
final list = [arg, arg];
PDLog.formated(list, level: LogLevel.info, toFile: true);
/// 检查指定日期的日志文件
final date = DateTime(2025, 7, 5);
final dayPath = await PDLog.logFilePathIfExists(date);
if (dayPath.isNotEmpty) {
// 文件存在
_readFileContent(PDLogFile(
path: dayPath,
sizeBytes: 1024,
modifiedMs: 1760172725000,
));
} else {
PDLog.e('${date.toIso8601String()}日期没有日志文件.');
}
final logFiles = await PDLog.listLogFilesByYear(2025);
PDLog.v(logFiles);
}
/// 压力测试:批量写入大量日志以验证缓冲与刷新表现。
///
/// 注意:为避免控制台性能影响,此处禁用控制台输出,仅写入原生缓冲与文件。
Future<void> _runStressTest() async {
const total = 1000;
final started = DateTime.now();
PDLog.v('压力测试开始: $total 条, started=$started', tag: 'Stress');
for (var i = 0; i < total; i++) {
final msg = '压力测试第 $i 条消息\n多行样式验证:第 ${(i % 3) + 1} 行';
// 使用自定义输出以覆盖控制台/文件开关,避免刷屏影响性能
PDLog.out(
msg,
tag: 'Stress',
useConsole: false,
useNative: true,
toFile: false,
showTimestamp: true,
style: const LogStyleConfig(
foreground: 36, // 青色
styles: [1], // 粗体
),
);
}
await PDLog.flushNativeLogs();
final elapsed = DateTime.now().difference(started);
PDLog.v('压力测试完成,用时 ${elapsed.inMilliseconds} ms', tag: 'Stress');
_refreshFiles();
}
/// 应用当前配置到原生日志系统(刷新间隔、缓冲阈值等)。
void _applyConfig() {
final flushMs = int.tryParse(_flushIntervalCtrl.text) ?? 2000;
final maxEntries = int.tryParse(_maxEntriesCtrl.text) ?? 100;
final maxBytes = int.tryParse(_maxBytesCtrl.text) ?? 65536;
PDLog.updateConfigure(
nativeFileLoggingEnabled: _nativeFileEnabled,
nativeFileLoggingFlushIntervalMs: flushMs,
nativeFileLoggingMaxBufferEntries: maxEntries,
nativeFileLoggingMaxBufferBytes: maxBytes,
);
PDLog.i(
'Applied file logging config: enabled=$_nativeFileEnabled, interval=${flushMs}ms, entries=$maxEntries, bytes=$maxBytes',
);
}
/// 立即触发原生侧刷新,将缓冲区写入磁盘并刷新文件列表。
Future<void> _flushNow() async {
await PDLog.flushNativeLogs();
_refreshFiles();
}
/// 获取日志根路径并打印年份目录(仅一级目录)。
Future<void> _refreshRootPath() async {
final path = await PDLog.logRootPath();
setState(() => _logRootPath = path);
final years = await PDLog.listYearFolders();
for (String e in years) {
PDLog.d(e);
}
}
/// 列出所有日志文件,计算总大小并更新界面。
Future<void> _refreshFiles() async {
final files = await PDLog.listLogFiles();
for (PDLogFile e in files) {
PDLog.d(e.toJson());
}
final total = files.fold<int>(0, (sum, f) => sum + f.sizeBytes);
setState(() {
_files = files;
_totalSize = total;
});
}
/// 读取指定日志文件内容并打印到控制台。
Future<void> _readFileContent(PDLogFile f) async {
try {
final file = File(f.path);
if (await file.exists()) {
final content = await file.readAsString();
if (kDebugMode) {
print('=== 文件内容: ${f.path} ===');
PDLog.v(content);
print('=== 文件内容结束 ===');
}
} else {
if (kDebugMode) {
print('文件不存在: ${f.path}');
}
}
} catch (e) {
if (kDebugMode) {
print('读取文件失败: $e');
}
}
}
/// 删除指定日志文件并刷新列表。
Future<void> _deleteFile(PDLogFile f) async {
final ok = await PDLog.deleteLogFile(f.path);
if (ok) {
_refreshFiles();
}
}
/// 删除所有日志文件并刷新列表。
Future<void> _deleteAllLogs() async {
final n = await PDLog.deleteAllLogFiles();
PDLog.w('Deleted $n log files');
_refreshFiles();
}
}