Template
chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库 7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系 5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter 通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sunny_mochi/core/crash/pending_crash_report.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'crash_reporter.g.dart';
|
||||
|
||||
/// Riverpod provider:崩溃报告在 main.dart 通过 overrideWithValue 注入。
|
||||
/// 默认值为 null(正常启动时无崩溃)。
|
||||
@Riverpod(keepAlive: true)
|
||||
PendingCrashReport? pendingCrash(Ref ref) => null;
|
||||
|
||||
/// 全局崩溃日志服务(纯静态,不依赖 Riverpod / Flutter framework)。
|
||||
///
|
||||
/// **生命周期**:
|
||||
/// 1. `preInit()` — main() 最早期调用,异步缓存路径 + 设备信息
|
||||
/// 2. `consumePending()` — 读取并删除上次崩溃文件(一次性消费)
|
||||
/// 3. `installHooks()` — 在 SentrySetup.init appRunner 内调用,链式挂载钩子
|
||||
///
|
||||
/// **崩溃写入**:仅用 `File.writeAsStringSync(flush: true)`,绝对不能 await。
|
||||
abstract class CrashReporter {
|
||||
static String? _crashFilePath;
|
||||
static String _deviceModel = 'unknown';
|
||||
static String _osVersion = 'unknown';
|
||||
static String _appVersion = 'unknown';
|
||||
static String _appBuild = '0';
|
||||
|
||||
/// 步骤 1:异步预初始化(收集路径 + 设备信息)。
|
||||
static Future<void> preInit() async {
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
_crashFilePath = p.join(dir.path, 'crash_report.json');
|
||||
|
||||
final pkg = await PackageInfo.fromPlatform();
|
||||
_appVersion = pkg.version;
|
||||
_appBuild = pkg.buildNumber;
|
||||
|
||||
if (kIsWeb) return;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
_deviceModel = '${info.manufacturer} ${info.model}'.trim();
|
||||
_osVersion = 'Android ${info.version.release}';
|
||||
} else if (Platform.isIOS) {
|
||||
final info = await DeviceInfoPlugin().iosInfo;
|
||||
_deviceModel = info.utsname.machine;
|
||||
_osVersion = '${info.systemName} ${info.systemVersion}';
|
||||
} else {
|
||||
_deviceModel = Platform.operatingSystem;
|
||||
_osVersion = Platform.operatingSystemVersion;
|
||||
}
|
||||
} catch (_) {
|
||||
// preInit 失败不能阻断启动流程
|
||||
}
|
||||
}
|
||||
|
||||
/// 步骤 2:一次性消费崩溃文件(读取 + 立即删除)。
|
||||
/// 返回 null 表示本次是正常启动。
|
||||
static PendingCrashReport? consumePending() {
|
||||
final path = _crashFilePath;
|
||||
if (path == null) return null;
|
||||
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) return null;
|
||||
|
||||
try {
|
||||
final raw = file.readAsStringSync();
|
||||
file.deleteSync();
|
||||
return PendingCrashReport.fromRawJson(raw);
|
||||
} catch (_) {
|
||||
try {
|
||||
file.deleteSync();
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 步骤 3:安装全局崩溃钩子(必须在 SentrySetup appRunner 内调用)。
|
||||
///
|
||||
/// 采用链式挂载:先读出 Sentry 已设置的 handler,我们包一层写回,
|
||||
/// 确保 Sentry 依然能接到所有错误。
|
||||
static void installHooks() {
|
||||
final previousOnError = PlatformDispatcher.instance.onError;
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
_writeCrashSync(kind: 'dart', error: error, stackTrace: stack);
|
||||
return previousOnError?.call(error, stack) ?? false;
|
||||
};
|
||||
}
|
||||
|
||||
/// 同步写入崩溃文件(crash hook 专用,不能有任何 await)。
|
||||
static void _writeCrashSync({
|
||||
required String kind,
|
||||
required Object error,
|
||||
required StackTrace stackTrace,
|
||||
}) {
|
||||
final path = _crashFilePath;
|
||||
if (path == null) return;
|
||||
|
||||
try {
|
||||
final stackStr = stackTrace.toString();
|
||||
final truncated = stackStr.length > 4096
|
||||
? '${stackStr.substring(0, 4096)}\n...(truncated)'
|
||||
: stackStr;
|
||||
|
||||
final report = PendingCrashReport(
|
||||
kind: kind,
|
||||
error: error.toString(),
|
||||
stackTrace: truncated,
|
||||
occurredAt: DateTime.now().toUtc().toIso8601String(),
|
||||
deviceModel: _deviceModel,
|
||||
osVersion: _osVersion,
|
||||
appVersion: _appVersion,
|
||||
appBuild: _appBuild,
|
||||
);
|
||||
|
||||
File(path).writeAsStringSync(
|
||||
report.toRawJson(),
|
||||
flush: true,
|
||||
);
|
||||
} catch (_) {
|
||||
// crash hook 内绝对不能抛出异常
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user