Files

149 lines
6.6 KiB
Dart
Raw Permalink Normal View History

2026-08-17 15:29:55 +08:00
/// 三个入口共用的启动编排。
///
/// 来源: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()),
);
}