Files
kitchen/lib/main.dart
2026-04-21 10:14:37 +08:00

376 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:catcher_2/catcher_2.dart';
import 'package:mom_kitchen/src/l10n/app_localizations.dart';
import 'package:mom_kitchen/src/config/app_routes.dart';
import 'package:mom_kitchen/src/services/core/app_service.dart';
import 'package:mom_kitchen/src/services/api/api_service.dart';
import 'package:mom_kitchen/src/services/system/orientation_service.dart';
import 'package:mom_kitchen/src/services/ui/theme_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
import 'package:mom_kitchen/src/services/data/storage/storage_service.dart';
import 'package:mom_kitchen/src/services/system/crash_guard_service.dart';
import 'package:mom_kitchen/src/app_binding.dart';
import 'package:mom_kitchen/src/utils/app_logger.dart';
// 2026-04-15 | main.dart | 应用入口 | Catcher2最先初始化+串行化启动流程防ANR
// 2026-04-15 | 增强防御:所有初始化步骤加超时+try-catch任何一步失败都不阻断启动
// 2026-04-15 | Trae IDE兼容增强错误处理防止调试环境闪退
// 2026-04-19 | 移除flutter_dotenv依赖不再加载.env文件
void main() {
runZonedGuarded(
() {
final crashGuard = CrashGuardService();
Catcher2(
runAppFunction: () async {
WidgetsFlutterBinding.ensureInitialized();
PlatformDispatcher.instance.onError = (error, stack) {
debugPrint('🚨 PlatformDispatcher Error: $error');
try {
Catcher2.reportCheckedError(error, stack);
} catch (_) {
debugPrint('❌ PlatformDispatcher report failed: $error');
}
return true;
};
FlutterError.onError = (details) {
if (_isLayoutOverflow(details)) {
debugPrint('⚠️ [LayoutWarning] ${details.exceptionAsString()}');
if (details.stack != null) {
debugPrint('⚠️ [LayoutWarning] Stack: ${details.stack}');
}
if (!kReleaseMode) {
FlutterError.presentError(details);
}
try {
Catcher2.reportCheckedError(details.exception, details.stack);
} catch (_) {
debugPrint(
'❌ FlutterError.onError catch failed: ${details.exception}',
);
}
return;
}
FlutterError.presentError(details);
try {
Catcher2.reportCheckedError(details.exception, details.stack);
} catch (_) {
debugPrint(
'❌ FlutterError.onError catch failed: ${details.exception}',
);
}
};
try {
await _initApp();
} catch (e, st) {
debugPrint('🚨 _initApp failed: $e');
debugPrint('Stack: $st');
}
runApp(const MyApp());
},
ensureInitialized: false,
debugConfig: crashGuard.buildDebugOptions(),
releaseConfig: crashGuard.buildReleaseOptions(),
);
},
(error, stack) {
debugPrint('🚨 Uncaught async error: $error');
debugPrint('Stack: $stack');
},
);
}
/// 启动初始化链 - 每一步都有独立超时和try-catch
/// 核心原则任何一步失败都不阻断后续步骤保证App至少能启动
Future<void> _initApp() async {
// 1. 核心服务初始化(最关键,但不阻断)
try {
await AppService.instance.init().timeout(
const Duration(seconds: 10),
onTimeout: () => debugPrint('❌ AppService初始化超时(10s)'),
);
} catch (e) {
debugPrint('❌ AppService初始化失败: $e');
}
// 2. DNS预检非关键纯后台
try {
await ApiService().preCheckDns().timeout(
const Duration(seconds: 5),
onTimeout: () => debugPrint('⚠️ DNS预检超时(5s)'),
);
} catch (e) {
debugPrint('⚠️ DNS预检失败: $e');
}
// 3. CrashGuard初始化
try {
await CrashGuardService.init().timeout(const Duration(seconds: 3));
} catch (e) {
debugPrint('⚠️ CrashGuardService初始化失败或超时: $e');
}
if (kDebugMode) {
AppRoutes.registerAllPages();
}
await OrientationService().unlockOrientation();
}
class DesktopScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.stylus,
PointerDeviceKind.trackpad,
};
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
ThemeService _getThemeService() {
if (Get.isRegistered<ThemeService>()) {
return Get.find<ThemeService>();
}
return Get.put(ThemeService.instance, permanent: true);
}
@override
Widget build(BuildContext context) {
final themeService = _getThemeService();
return Obx(() {
final textScale = themeService.fontSize.value / 16.0;
return GetCupertinoApp(
title: 'Mom\'s Kitchen',
navigatorKey: Catcher2.navigatorKey,
theme: themeService.cupertinoThemeData,
locale: Locale(themeService.currentLocale.value),
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: const [
Locale('en'),
Locale('zh'),
Locale('zh', 'Hant'),
],
getPages: AppRoutes.pages,
initialBinding: AppBinding(),
builder: (context, widget) {
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (event) {
if (event is KeyDownEvent) {
final isAltLeft =
HardwareKeyboard.instance.isAltPressed &&
event.logicalKey == LogicalKeyboardKey.arrowLeft;
final isBackspace =
event.logicalKey == LogicalKeyboardKey.backspace;
final isBrowserBack =
event.logicalKey == LogicalKeyboardKey.browserBack;
if (isAltLeft || isBackspace || isBrowserBack) {
if (Get.currentRoute != '/' &&
Get.currentRoute != AppRoutes.main &&
Get.currentRoute != AppRoutes.guide) {
Get.back();
}
}
}
},
child: ScrollConfiguration(
behavior: DesktopScrollBehavior(),
child: MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.linear(textScale)),
child: ColoredBox(
color: themeService.backgroundColor.value,
child: widget ?? const SizedBox.shrink(),
),
),
),
);
},
routingCallback: (routing) {
if (kDebugMode && routing?.current != null) {
AppLogger.d('🔍 路由变化: ${routing?.previous}${routing?.current}');
}
},
home: const _InitWrapper(),
);
});
}
}
class _InitWrapper extends StatefulWidget {
const _InitWrapper();
@override
State<_InitWrapper> createState() => _InitWrapperState();
}
class _InitWrapperState extends State<_InitWrapper>
with WidgetsBindingObserver {
bool _navigated = false;
int _retryCount = 0;
static const int _maxRetries = 5;
String? _lastError;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_navigateToMain();
});
}
void _navigateToMain() {
if (!mounted || _navigated) return;
try {
ToastService.init(context);
final agreementAccepted =
StorageService().getBool('agreement_accepted') ?? false;
if (agreementAccepted) {
Get.offAllNamed(AppRoutes.main);
} else {
Get.offAllNamed(AppRoutes.guide);
}
_navigated = true;
debugPrint('✅ 导航到${agreementAccepted ? '主页面' : '引导页'}成功');
} catch (e, st) {
_lastError = e.toString();
_retryCount++;
debugPrint('❌ 导航失败 (第$_retryCount次): $e');
debugPrint('Stack: $st');
if (_retryCount < _maxRetries) {
Future.delayed(Duration(milliseconds: 300 * _retryCount), () {
if (mounted && !_navigated) _navigateToMain();
});
} else {
debugPrint('🚨 导航重试已达上限,显示错误页面');
if (mounted) setState(() {});
}
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
final brightness =
WidgetsBinding.instance.platformDispatcher.platformBrightness;
try {
final themeService = Get.find<ThemeService>();
themeService.onSystemBrightnessChanged(brightness);
} catch (_) {}
}
@override
Widget build(BuildContext context) {
if (_retryCount >= _maxRetries) {
return CupertinoPageScaffold(
child: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.exclamationmark_triangle,
size: 48,
color: CupertinoColors.systemRed,
),
const SizedBox(height: 16),
const Text(
'启动失败,请重试',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
if (_lastError != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey5,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_lastError!.length > 200
? '${_lastError!.substring(0, 200)}...'
: _lastError!,
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
),
textAlign: TextAlign.center,
),
),
],
const SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoButton(
onPressed: () {
_retryCount = 0;
_lastError = null;
setState(() {});
_navigateToMain();
},
child: const Text('重试'),
),
const SizedBox(width: 16),
CupertinoButton.filled(
onPressed: () {
_retryCount = 0;
_lastError = null;
_navigated = false;
setState(() {});
_navigateToMain();
},
child: const Text('强制重启'),
),
],
),
],
),
),
),
),
);
}
return const CupertinoPageScaffold(
child: Center(child: CupertinoActivityIndicator()),
);
}
}
bool _isLayoutOverflow(FlutterErrorDetails details) {
final msg = details.exceptionAsString().toLowerCase();
return msg.contains('overflowed') ||
msg.contains('renderflex') ||
msg.contains('renderconstrainedbox') ||
msg.contains('hasSize') ||
msg.contains('not positioned');
}