import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:patbond_flutter/analytics/analytics_page_name.dart'; import 'package:patbond_flutter/analytics/analytics_route_observer.dart'; import 'package:patbond_flutter/analytics/analytics_service.dart'; import 'package:patbond_flutter/analytics/page_view_tracker.dart'; import 'package:patbond_flutter/analytics/session_tracker.dart'; import 'package:patbond_flutter/app/app_localization.dart'; import 'package:patbond_flutter/core/network/api_client.dart'; import 'package:patbond_flutter/core/network/token_refresher.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/core/widgets/avatar_upload_sheet.dart'; import 'package:patbond_flutter/features/auth/auth_repository.dart'; import 'package:patbond_flutter/features/auth/login_page.dart'; import 'package:patbond_flutter/features/auth/session_manager.dart'; import 'package:patbond_flutter/features/auth/splash_page.dart'; import 'package:patbond_flutter/features/community/community_controller.dart'; import 'package:patbond_flutter/features/community/community_interaction_analytics.dart'; import 'package:patbond_flutter/features/community/community_repository.dart'; import 'package:patbond_flutter/features/community/feed_analytics.dart'; import 'package:patbond_flutter/features/community/media_uploader.dart'; import 'package:patbond_flutter/features/community/post_analytics.dart'; import 'package:patbond_flutter/features/main/main_shell_page.dart'; import 'package:patbond_flutter/features/pets/health_record_analytics.dart'; import 'package:patbond_flutter/features/pets/pet_analytics.dart'; import 'package:patbond_flutter/features/pets/pets_controller.dart'; import 'package:patbond_flutter/features/pets/pets_repository.dart'; import 'package:patbond_flutter/features/profile/profile_controller.dart'; import 'package:patbond_flutter/state/app_state.dart'; class App extends StatefulWidget { const App({ super.key, this.sessionManager, this.authRepository, this.petsRepository, this.communityRepository, this.mediaUploaderFactory, this.avatarUploaderFactory, }); /// 测试注入口;生产默认走安全存储 + 真实 API。 final SessionManager? sessionManager; final AuthRepository? authRepository; final PetsRepository? petsRepository; final CommunityRepository? communityRepository; /// 发布页媒体上传器构造口(桌面实测替换选图/压缩层;生产为 null)。 final MediaUploaderFactory? mediaUploaderFactory; /// 头像上传器构造口(T3.5-08/09)。生产为 null → 走 /// [defaultAvatarUploader];桌面实测与集成测试在此替换选图/压缩层 /// (Linux 桌面无 image_picker / 压缩原生实现),网络三段仍是生产实现。 final AvatarUploaderFactory? avatarUploaderFactory; @override State createState() => _AppState(); } class _AppState extends State { late final AppState appState; late final SessionManager sessionManager; late final AuthRepository authRepository; late final PetsController petsController; late final CommunityController communityController; late final ProfileController profileController; late final AvatarUploaderBuilder _avatarUploaderBuilder; late final PetAnalytics petAnalytics; late final HealthRecordAnalytics healthRecordAnalytics; late final FeedAnalytics feedAnalytics; late final CommunityInteractionAnalytics interactionAnalytics; late final PostAnalytics postAnalytics; late final SessionTracker _sessionTracker; late final AnalyticsService _analytics; late final PageViewTracker _pageViewTracker; late final AnalyticsRouteObserver _routeObserver; TokenRefresher? _sharedRefresher; @override void initState() { super.initState(); appState = AppState()..load(); sessionManager = widget.sessionManager ?? SessionManager(store: const SecureTokenStore()); _sessionTracker = SessionTracker( onLeaveForeground: () { // 退后台:停定时冲刷并立即冲刷一次(既有触发点保留)。 _analytics.stopPeriodicFlush(); _analytics.flushNow(); }, onEnterForeground: () => _analytics.startPeriodicFlush(), ); WidgetsBinding.instance.addObserver(_sessionTracker); _analytics = AnalyticsService( apiBaseUrl: patbondUserApiBaseUrl, getAccessToken: () => sessionManager.accessToken, getSessionId: () => _sessionTracker.sessionId, ); _initAppVersion(); // 冷启动恢复持久化埋点队列并冲刷离线积压(后台任务,不阻塞渲染)。 _analytics.restore(); // 前台期间 30 秒定时冲刷(13 号 §3.4 第 4 触发点)。 _analytics.startPeriodicFlush(); _pageViewTracker = PageViewTracker(_analytics.trackEvent); _routeObserver = AnalyticsRouteObserver( tracker: _pageViewTracker, resolveRootPage: _resolveRootPage, ); authRepository = widget.authRepository ?? _buildRepository(); petsController = PetsController( repository: widget.petsRepository ?? _buildPetsRepository(), ); // T3-14:Feed segment 接线主壳(数据层 T3-12 就位);T3-16 起 // 点赞/收藏成功埋点经 interactionAnalytics 在 controller 内上报。 interactionAnalytics = CommunityInteractionAnalytics(_analytics.trackEvent); final communityRepository = widget.communityRepository ?? _buildCommunityRepository(); communityController = CommunityController( repository: communityRepository, interactionAnalytics: interactionAnalytics, ); // T3.5-08/10:资料与统计单例,资料 Tab 与首页问候语共用(一次 /me // 供两个消费点,改昵称后两处同时变)。 profileController = ProfileController( authRepository: authRepository, communityRepository: communityRepository, ); // 头像上传:purpose 由调用页给定(user_avatar / pet_avatar)。仓库复用 // community 仓库——media 两步上传端点由 user 服务提供,已在 // _buildCommunityRepository 里单独接线(mediaApi)。 final avatarFactory = widget.avatarUploaderFactory ?? defaultAvatarUploader; _avatarUploaderBuilder = (purpose) => avatarFactory(communityRepository, purpose); petAnalytics = PetAnalytics(_analytics.trackEvent); healthRecordAnalytics = HealthRecordAnalytics(_analytics.trackEvent); feedAnalytics = FeedAnalytics(_analytics.trackEvent); // T3-17:发布漏斗五事件 + 媒体上传三段(发布页与 MediaUploader 消费)。 postAnalytics = PostAnalytics(_analytics.trackEvent); // 认证状态切换补点(根路由 AnimatedSwitcher 无路由事件) sessionManager.addListener(_reportAuthStateChange); } /// 异步读取 appVersion 并注入 analytics(启动后台任务,不阻塞渲染)。 Future _initAppVersion() async { try { final info = await PackageInfo.fromPlatform(); _analytics.setAppVersion('${info.version}+${info.buildNumber}'); } catch (error) { // 读取失败回退 'unknown'(AnalyticsService 构造默认值已兜底)。 } } /// 三服务分端口直连(ADR-002 无网关),token 刷新单飞经共享 /// [TokenRefresher](22 号报告 §7 交接约定)。 TokenRefresher _ensureRefresher() { return _sharedRefresher ??= TokenRefresher( dio: buildPatbondDio(session: sessionManager), session: sessionManager, ); } AuthRepository _buildRepository() { final dio = buildPatbondDio(session: sessionManager); final refresher = _ensureRefresher(); final api = ApiClient( dio: dio, session: sessionManager, refresher: refresher, ); // `/api/v1/me` 由 user 服务(:8082)提供,auth(:8081)上没有该路由, // 故单独接一条 user 线路(与 community 仓库的 mediaApi 同构)。 final userApi = ApiClient( dio: buildPatbondDio( session: sessionManager, baseUrl: patbondUserApiBaseUrl, ), session: sessionManager, refresher: refresher, ); return ApiAuthRepository( api: api, userApi: userApi, session: sessionManager, refresher: refresher, analytics: _analytics, ); } PetsRepository _buildPetsRepository() { final dio = buildPatbondDio( session: sessionManager, baseUrl: patbondPetApiBaseUrl, ); final api = ApiClient( dio: dio, session: sessionManager, refresher: _ensureRefresher(), ); return ApiPetsRepository(api: api); } /// community 服务分端口直连(:8084);media 两步上传端点由 user 服务 /// (:8082 MediaController)提供,单独建 mediaApi 直连。token 刷新 /// 单飞经共享 [TokenRefresher]。 CommunityRepository _buildCommunityRepository() { final dio = buildPatbondDio( session: sessionManager, baseUrl: patbondCommunityApiBaseUrl, ); final api = ApiClient( dio: dio, session: sessionManager, refresher: _ensureRefresher(), ); final mediaDio = buildPatbondDio( session: sessionManager, baseUrl: patbondUserApiBaseUrl, ); final mediaApi = ApiClient( dio: mediaDio, session: sessionManager, refresher: _ensureRefresher(), ); return ApiCommunityRepository(api: api, mediaApi: mediaApi); } AnalyticsPageName? _resolveRootPage() { // 回栈到无名根路由时解析当前认证状态页/主壳 Tab return switch (sessionManager.status) { AuthStatus.unknown => null, AuthStatus.unauthenticated => AnalyticsPageName.login, AuthStatus.authenticated => _pageViewTracker.currentTab, }; } void _reportAuthStateChange() { // 登出即清宠物档案、社区 Feed 与本人资料内存副本(跨账号不泄漏; // 重登后重新拉取)。 if (sessionManager.status == AuthStatus.unauthenticated) { petsController.reset(); communityController.reset(); profileController.reset(); } // 认证状态机切页补点(03 §3.2 非路由曝光 1/2) final page = switch (sessionManager.status) { AuthStatus.unauthenticated => AnalyticsPageName.login, AuthStatus.authenticated => _pageViewTracker.currentTab, AuthStatus.unknown => null, }; if (page != null) _pageViewTracker.report(page); } @override void dispose() { _analytics.stopPeriodicFlush(); sessionManager.removeListener(_reportAuthStateChange); WidgetsBinding.instance.removeObserver(_sessionTracker); appState.dispose(); petsController.dispose(); communityController.dispose(); profileController.dispose(); if (widget.sessionManager == null) sessionManager.dispose(); super.dispose(); } Widget _homeForStatus() { switch (sessionManager.status) { case AuthStatus.unknown: return SplashPage( key: const ValueKey('splash'), authRepository: authRepository, sessionManager: sessionManager, ); case AuthStatus.unauthenticated: return LoginPage( key: const ValueKey('login'), authRepository: authRepository, ); case AuthStatus.authenticated: return MainShellPage( key: const ValueKey('shell'), appState: appState, petsController: petsController, communityController: communityController, profileController: profileController, currentUserId: sessionManager.userId, petAnalytics: petAnalytics, healthRecordAnalytics: healthRecordAnalytics, feedAnalytics: feedAnalytics, interactionAnalytics: interactionAnalytics, postAnalytics: postAnalytics, mediaUploaderFactory: widget.mediaUploaderFactory, avatarUploaderBuilder: _avatarUploaderBuilder, pageViewTracker: _pageViewTracker, onLogout: authRepository.logout, ); } } @override Widget build(BuildContext context) { return MaterialApp( title: 'Patbond', debugShowCheckedModeBanner: false, theme: buildAppTheme(), // M3.5-01:Material 内置组件中文化(此前未配 delegate,日期选择器等 // 全部回退英文兜底)。单语言 zh-CN,不随设备语言回退英文。 localizationsDelegates: appLocalizationsDelegates, supportedLocales: appSupportedLocales, locale: appLocale, navigatorObservers: [_routeObserver], home: ListenableBuilder( listenable: sessionManager, builder: (context, _) { // 认证状态切换统一 300ms 淡入淡出(Splash ↔ 登录 ↔ 主壳, // BrandMark 同构保证品牌区过渡对位,组装稿 §7)。 return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: _homeForStatus(), ); }, ), ); } }