Files
2026-08-17 15:29:55 +08:00

149 lines
6.6 KiB
Dart
Raw Permalink 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.
/// 三个入口共用的启动编排。
///
/// 来源: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<void> 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: <ProviderObserver>[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) => <SessionScopedStore>[ref.watch(webViewSessionProvider)],
),
sessionObserversProvider.overrideWith(
(Ref ref) => <SessionObserver>[
AppSessionObserver(
analytics: ref.watch(analyticsProvider),
reporter: ref.watch(crashReporterProvider),
),
],
),
// --- 路由聚合:core_router 不认识任何 feature,在这里拼 -------------
appRoutesProvider.overrideWith(
(Ref ref) => <RouteBase>[...buildAuthRoutes(), ...buildHomeRoutes()],
),
navigatorObserversProvider.overrideWith(
(Ref ref) => <NavigatorObserver>[
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): 神策采购未落地,暂用 NoopAnalyticscore_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.onErrorSentryFlutter 已经在
// 里面装好了 Flutter / PlatformDispatcher / Zone 三层钩子,再手写一遍
// 会**每个异常上报两次**(13)。
appRunner: () => runApp(buildApp()),
);
}