Flutter应用崩溃时日志存储及向开发者发送功能实现方法
在Flutter中实现崩溃日志存储、查看与上报方案
嘿,这个需求在Flutter开发里挺常见的,我整理了一套从日志存储到崩溃上报的完整流程,亲测好用:
一、基础准备:依赖包引入
首先在pubspec.yaml里添加几个实用的包:
dependencies: path_provider: ^2.1.1 # 获取本地存储路径 logger: ^2.0.2+1 # 格式化日志(可选,但能让日志更清晰) mailer: ^6.1.0 # 发送邮件上报日志(可选,也可以用自己的后端接口)
二、实现本地日志存储与全局崩溃捕获
我们需要一个全局的日志管理类,负责写入日志到本地文件,同时捕获应用的所有异常:
1. 日志管理类示例
import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:logger/logger.dart'; class LogManager { static final LogManager _instance = LogManager._internal(); factory LogManager() => _instance; LogManager._internal(); late File _logFile; final Logger _logger = Logger(); // 初始化日志文件(建议在app启动时调用) Future<void> init() async { final directory = await getApplicationDocumentsDirectory(); final String logFileName = "app_log_${DateTime.now().toIso8601String().split('T')[0]}.txt"; _logFile = File('${directory.path}/$logFileName'); // 确保文件存在 if (!await _logFile.exists()) { await _logFile.create(recursive: true); } // 启动全局异常捕获 _setupErrorHandling(); } // 全局异常捕获配置 void _setupErrorHandling() { // 捕获Flutter框架层面的异常 FlutterError.onError = (FlutterErrorDetails details) { _logError(details.exception, details.stack); // 如果是发布版,不要直接抛出,避免应用直接崩溃(可选) if (!kDebugMode) { FlutterError.presentError(details); } }; // 捕获Isolate中的异常(比如后台任务的错误) Isolate.current.addErrorListener(RawReceivePort((pair) async { final List<dynamic> errorAndStacktrace = pair; _logError(errorAndStacktrace[0], errorAndStacktrace[1]); }).sendPort); // 捕获未处理的异步错误 runZonedGuarded(() { // 这里放你的app启动逻辑,比如runApp(MyApp()) }, (error, stackTrace) { _logError(error, stackTrace); }); } // 写入日志到文件 Future<void> _logError(dynamic error, dynamic stackTrace) async { final String logContent = """ ======================================== 时间: ${DateTime.now().toString()} 错误信息: $error 堆栈跟踪: $stackTrace ======================================== """; // 写入文件 await _logFile.writeAsString(logContent, mode: FileMode.append); // 同时在控制台打印(调试用) _logger.e(logContent); } // 获取所有日志文件(用于查看) Future<List<File>> getLogFiles() async { final directory = await getApplicationDocumentsDirectory(); final files = directory.listSync().whereType<File>().toList(); // 筛选出日志文件 return files.where((file) => file.path.endsWith('.txt') && file.path.contains('app_log')).toList(); } // 读取指定日志文件的内容 Future<String> readLogFile(File file) async { return await file.readAsString(); } }
2. 在App启动时初始化
在main.dart里调用初始化:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await LogManager().init(); runApp(const MyApp()); }
三、实现崩溃时查看日志的功能
你可以做一个隐藏的入口来触发日志查看页面,比如:
- 连续点击应用图标5次(适合测试版)
- 在设置页面添加一个“查看日志”按钮(适合内部测试应用)
日志查看页面示例
class LogViewerPage extends StatefulWidget { const LogViewerPage({super.key}); @override State<LogViewerPage> createState() => _LogViewerPageState(); } class _LogViewerPageState extends State<LogViewerPage> { List<File> _logFiles = []; String? _selectedLogContent; @override void initState() { super.initState(); _loadLogFiles(); } Future<void> _loadLogFiles() async { final files = await LogManager().getLogFiles(); // 按时间倒序排列,最新的日志在前 files.sort((a, b) => b.path.compareTo(a.path)); setState(() { _logFiles = files; // 默认加载最新的日志 if (_logFiles.isNotEmpty) { _readLogFile(_logFiles.first); } }); } Future<void> _readLogFile(File file) async { final content = await LogManager().readLogFile(file); setState(() { _selectedLogContent = content; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('应用日志')), body: Column( children: [ // 日志文件列表 SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _logFiles.length, itemBuilder: (context, index) { final file = _logFiles[index]; final fileName = file.path.split('/').last; return Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: () => _readLogFile(file), child: Text(fileName), ), ); }, ), ), // 日志内容展示 Expanded( child: _selectedLogContent != null ? SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), child: Text(_selectedLogContent!), ), ) : const Center(child: Text('暂无日志')), ), ], ), ); } }
四、实现日志发送给开发者的功能
这里提供两种常见方案:
方案1:调用系统邮件应用发送
不需要额外依赖,直接用url_launcher打开邮件链接,把日志内容作为正文:
import 'package:url_launcher/url_launcher.dart'; Future<void> sendLogViaEmail(String logContent) async { final Uri emailLaunchUri = Uri( scheme: 'mailto', path: 'developer@yourapp.com', queryParameters: { 'subject': '应用崩溃日志 - ${DateTime.now().toString()}', 'body': logContent, }, ); if (await canLaunchUrl(emailLaunchUri)) { await launchUrl(emailLaunchUri); } else { throw '无法打开邮件应用'; } }
方案2:通过邮件API直接发送(用mailer包)
适合不需要用户手动操作的自动上报:
import 'package:mailer/mailer.dart'; import 'package:mailer/smtp_server.dart'; Future<void> sendLogAutomatically(String logContent) async { final smtpServer = gmail('your-email@gmail.com', 'your-app-password'); // 需要开启Gmail的应用密码 final message = Message() ..from = const Address('your-email@gmail.com', 'App Crash Reporter') ..recipients.add('developer@yourapp.com') ..subject = '应用自动上报崩溃日志 - ${DateTime.now().toString()}' ..text = logContent; try { await send(message, smtpServer); print('日志发送成功'); } on MailerException catch (e) { print('日志发送失败: ${e.toString()}'); } }
五、注意事项
- 发布版配置:在Android的
AndroidManifest.xml和iOS的Info.plist中确保有必要的权限(比如存储权限,如果用外部存储的话;邮件权限如果需要的话) - 日志清理:可以定期清理旧日志,比如保留最近7天的日志,避免占用过多存储空间
- 敏感信息:日志中不要包含用户的敏感数据(比如密码、手机号),上报前要做过滤处理
内容的提问来源于stack exchange,提问作者Daniel Mana
相关产品推荐
相关产品推荐

