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()),
|
|||
|
|
);
|
|||
|
|
}
|