Files
patbond-flutter/lib/app/app.dart
T
lixi 19bd8c1810
CI / flutter-gates (push) Successful in 2m26s
新增:community feature 数据层——契约 v1.3.0 十九操作全覆盖 + ToggleSync 乐观更新状态机(T3-12)
- community_models:community/media 域 DTO 逐字段照冻结契约手写 JSON 映射
  (Post/FeedCard/PostComment/AuthorSummary 降级形态/媒体两步上传凭据等),
  未知枚举抛 FormatException 暴露契约漂移;CursorPage 上移 core 复用
- community_repository:13 路径 19 操作全覆盖;createPost/createComment
  必带 Idempotency-Key(每次逻辑提交换新键、刷新重放同键);点赞/收藏/
  关注走 PUT/DELETE 语义幂等
- community_exceptions:v1.3.0 新增 9 码 + 40902 共码类型化异常映射
- toggle_sync:乐观翻转 + 快照回滚 + 单飞合并最终意图 + 代次守卫,
  点赞/收藏共用一套参数化状态机,权威终态对账收敛
- community_controller:Feed 多页缓存 + 首屏四态 + 尾部加载三态 +
  游标拼接 + 刷新代次丢弃旧尾页;详情副本与卡片互动状态同源;reset 清态
- app.dart 装配:community 服务分端口直连(:8084),共享 TokenRefresher,
  登出同步 reset
- 测试 286 → 347(模型映射 / 19 操作线路 / 错误映射 / 竞态序列全覆盖)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-09 12:01:25 +08:00

252 lines
8.8 KiB
Dart
Raw 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.
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/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/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_repository.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/state/app_state.dart';
class App extends StatefulWidget {
const App({
super.key,
this.sessionManager,
this.authRepository,
this.petsRepository,
this.communityRepository,
});
/// 测试注入口;生产默认走安全存储 + 真实 API。
final SessionManager? sessionManager;
final AuthRepository? authRepository;
final PetsRepository? petsRepository;
final CommunityRepository? communityRepository;
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
late final AppState appState;
late final SessionManager sessionManager;
late final AuthRepository authRepository;
late final PetsController petsController;
late final CommunityController communityController;
late final PetAnalytics petAnalytics;
late final HealthRecordAnalytics healthRecordAnalytics;
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-12 只装配数据层;Feed segment 的 UI 接线在 T3-14 挂入主壳。
communityController = CommunityController(
repository: widget.communityRepository ?? _buildCommunityRepository(),
);
petAnalytics = PetAnalytics(_analytics.trackEvent);
healthRecordAnalytics = HealthRecordAnalytics(_analytics.trackEvent);
// 认证状态切换补点(根路由 AnimatedSwitcher 无路由事件)
sessionManager.addListener(_reportAuthStateChange);
}
/// 异步读取 appVersion 并注入 analytics(启动后台任务,不阻塞渲染)。
Future<void> _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,
);
return ApiAuthRepository(
api: api,
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),token 刷新单飞经共享 [TokenRefresher]。
CommunityRepository _buildCommunityRepository() {
final dio = buildPatbondDio(
session: sessionManager,
baseUrl: patbondCommunityApiBaseUrl,
);
final api = ApiClient(
dio: dio,
session: sessionManager,
refresher: _ensureRefresher(),
);
return ApiCommunityRepository(api: api);
}
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();
}
// 认证状态机切页补点(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();
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,
petAnalytics: petAnalytics,
healthRecordAnalytics: healthRecordAnalytics,
pageViewTracker: _pageViewTracker,
onLogout: authRepository.logout,
);
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Patbond',
debugShowCheckedModeBanner: false,
theme: buildAppTheme(),
navigatorObservers: [_routeObserver],
home: ListenableBuilder(
listenable: sessionManager,
builder: (context, _) {
// 认证状态切换统一 300ms 淡入淡出(Splash ↔ 登录 ↔ 主壳,
// BrandMark 同构保证品牌区过渡对位,组装稿 §7)。
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _homeForStatus(),
);
},
),
);
}
}