/// 三个入口共用的启动编排。 /// /// 来源:08(多环境)、13(Sentry / 日志)、12(全局错误)、03(Riverpod 装配)。 library; import 'package:app/src/app_widget.dart'; import 'package:app/src/device_id.dart'; import 'package:app/src/error_observer.dart'; import 'package:app/src/h5_launch_repository.dart'; import 'package:app/src/session_observer.dart'; import 'package:core_analytics/core_analytics.dart'; import 'package:core_auth/core_auth.dart'; import 'package:core_foundation/core_foundation.dart'; import 'package:core_logging/core_logging.dart'; import 'package:core_network/core_network.dart'; import 'package:core_router/core_router.dart'; import 'package:core_storage/core_storage.dart'; import 'package:core_webview/core_webview.dart'; import 'package:feature_auth/feature_auth.dart'; import 'package:feature_home/feature_home.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; /// TODO(app): 换成 package_info_plus 读真实版本,现在写死会在灰度期骗人。 const String _appVersion = '1.0.0+1'; /// 全部三个 main_*.dart 都只调这一个函数。 /// /// --------------------------------------------------------------------------- /// 这个函数是**整个仓库唯一一处知道所有包的地方**。各个 core_* 只声明自己需要 /// 什么(端口 + provider),谁来满足它在这里决定——这就是那些 /// `throw UnimplementedError('必须在 bootstrap 里 override')` 的兑现点。 /// /// 每加一个 override 就等于在编译期之外多了一个"忘了接就炸"的风险,所以下面 /// 每一条都标注了它兑现的是哪个端口。 /// --------------------------------------------------------------------------- Future bootstrap(AppEnv env) async { WidgetsFlutterBinding.ensureInitialized(); // 装一次全局单例,供拿不到 Ref 的 TokenRefresher 用;重复调用会抛错。 AppEnv.install(env); final LogBuffer buffer = LogBuffer(); final AppLogger logger = LoggerAppLogger(env: env, buffer: buffer); // dsn 为空(dev 默认)时连 SDK 都不初始化:开发期的噪音不该混进线上数据。 final bool sentryEnabled = env.sentryDsn.isNotEmpty; final CrashReporter reporter = sentryEnabled ? const SentryCrashReporter() : const NoopCrashReporter(); final Prefs prefs = Prefs(); final ClientInfo clientInfo = ClientInfo( appVersion: _appVersion, deviceId: await loadOrCreateDeviceId(prefs), ); // 12 §五:默认的红屏在 release 里是白屏加一行英文,用户只会以为 App 坏了。 ErrorWidget.builder = (FlutterErrorDetails details) => env.isProd ? const Material(child: Center(child: Text('页面出了点问题,请退出重试'))) : ErrorWidget(details.exception); Widget buildApp() { return ProviderScope( observers: [ErrorObserver(logger: logger, reporter: reporter)], overrides: [ // --- 基础设施:值已经在上面造好了,直接注入 ----------------------- appEnvProvider.overrideWithValue(env), // core_foundation appLoggerProvider.overrideWithValue(logger), // core_logging logBufferProvider.overrideWithValue(buffer), // core_logging(和 beforeSend 同一实例) crashReporterProvider.overrideWithValue(reporter), // core_logging prefsProvider.overrideWithValue(prefs), // core_storage clientInfoProvider.overrideWithValue(clientInfo), // core_network 端口 // --- 端口:接口在 core_*,实现在能依赖 core_network 的这一层 -------- sessionRemoteProvider.overrideWith( (Ref ref) => ref.watch(authRepositoryProvider), // core_auth ← feature_auth ), h5LaunchRepositoryProvider.overrideWith( (Ref ref) => ApiH5LaunchRepository(ref.watch(apiClientProvider)), // core_webview ← app ), // --- 会话级联的参与者 --------------------------------------------- // 登出/切店时要被清掉的东西在这里登记。**漏登记 = 上一个用户的数据 // 留在设备上**,而门店设备是共用的(11)。 sessionScopedStoresProvider.overrideWith( (Ref ref) => [ref.watch(webViewSessionProvider)], ), sessionObserversProvider.overrideWith( (Ref ref) => [ AppSessionObserver( analytics: ref.watch(analyticsProvider), reporter: ref.watch(crashReporterProvider), ), ], ), // --- 路由聚合:core_router 不认识任何 feature,在这里拼 ------------- appRoutesProvider.overrideWith( (Ref ref) => [...buildAuthRoutes(), ...buildHomeRoutes()], ), navigatorObserversProvider.overrideWith( (Ref ref) => [ CrashBreadcrumbObserver(ref.watch(crashReporterProvider)), ], ), routeReporterProvider.overrideWith( (Ref ref) => AppRouteReporter( logger: ref.watch(appLoggerProvider), reporter: ref.watch(crashReporterProvider), ), ), // --- 日志出口:core_network 只知道"往这里写字符串" ------------------- apiLogSinkProvider.overrideWith((Ref ref) { final AppLogger sink = ref.watch(appLoggerProvider); return (String message) => sink.d(message); }), // TODO(analytics): 神策采购未落地,暂用 NoopAnalytics(core_analytics 的默认值)。 // 接入时在这里 override,且必须在**用户同意隐私政策之后**才初始化 SDK(13)。 ], child: const ContiApp(), ); } if (!sentryEnabled) { runApp(buildApp()); return; } await SentryFlutter.init( (SentryFlutterOptions options) { options.dsn = env.sentryDsn; options.environment = env.flavor.name; options.release = 'conti-retail-app@$_appVersion'; options.tracesSampleRate = env.isProd ? 0.1 : 1.0; // 合规红线:不自动带用户 IP / 请求头 / cookie。 options.sendDefaultPii = false; options.beforeBreadcrumb = scrubBreadcrumb; options.beforeSend = buildScrubEvent(buffer); options.debug = false; }, // 用 appRunner 而不是自己写 FlutterError.onError:SentryFlutter 已经在 // 里面装好了 Flutter / PlatformDispatcher / Zone 三层钩子,再手写一遍 // 会**每个异常上报两次**(13)。 appRunner: () => runApp(buildApp()), ); }