149 lines
6.6 KiB
Dart
149 lines
6.6 KiB
Dart
/// 三个入口共用的启动编排。
|
||
///
|
||
/// 来源: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): 神策采购未落地,暂用 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()),
|
||
);
|
||
}
|