You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:16:44