// lib/observer/page_lifecycle_observer.dart import 'dart:async'; import 'package:analytics_sdk/config/sdk_config.dart'; import 'package:analytics_sdk/entity/app_page_view_event.dart'; import 'package:analytics_sdk/manager/session_metrics_manager.dart'; import 'package:analytics_sdk/utils/logger.dart'; import 'package:analytics_sdk/manager/page_name_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; /// 路由页面生命周期观察者,维护页面状态并按配置自动追踪页面展示事件。 /// /// **职责** /// - 监听 [Navigator] 的 push / pop / remove / replace,记录 Session 页面停留 /// - [autoTrackPageViews] 开启时,在渲染完成后自动上报 [AppPageViewEvent] /// - 维护 [currentPageKey],供 [GlobalClickWrapper] 进行 page_click 点击归属 /// - 通过 [_PageInfo.effectivePageKey] 支持子页面精准归属: /// Tab / PageView 切换时更新栈顶的 effectivePageKey,pop 返回时自动恢复正确 key /// /// **接入**:在 [MaterialApp.navigatorObservers] 注册 SDK 提供的 `pageObserver` /// 或 `sessionObserver`,无需手动实例化。 /// /// **限制**:仅追踪带名称的 [PageRoute]([RouteSettings.name] 非空); /// Dialog、BottomSheet 等非 PageRoute 不会触发上报。 class PageLifecycleObserver extends NavigatorObserver { /// 全局路由堆栈(静态,确保 [recordNavigation] 等静态方法可直接访问)。 /// /// 每个 [_PageInfo] 元素记录路由 key、来路、进入时间以及当前有效子页面 key。 static final List<_PageInfo> _pageStack = []; /// 事件上报回调(由 AnalyticsSdk 在组合根注入,避免循环依赖) final void Function(dynamic event) _track; /// 用户类型提供者(由 AnalyticsSdk 在组合根注入) final String Function() _getUserType; /// 页面退出回调,传出退出页面的 pageKey(用于清除页面级广告去重状态) final void Function(String pageKey)? _onPageExit; /// 真实路由解析回调(由外部注入,例如 GetX 反向匹配模板路由) String Function(String? actualRoute)? _resolveRealRoute; /// 是否自动上报 [AppPageViewEvent]。 /// /// `false` 时仍维护页面状态并写入 Session 停留明细,供手动曝光模式使用。 final bool autoTrackPageViews; /// 页面堆栈最大容量限制,防止内存无限增长 static int get _maxPageStackSize => SdkConfig.maxPageStackSize; /// 当前有效页面 key,供 [GlobalClickWrapper] 及业务方读取。 /// /// 更新时机: /// - 路由 push → 更新为新路由 key /// - 路由 pop → 恢复为目标路由的 [_PageInfo.effectivePageKey](含子页面状态) /// - [recordNavigation] → Tab 切换或 PageView 翻页时手动更新 static final ValueNotifier _currentPageKeyNotifier = ValueNotifier('main'); /// 当前有效页面 key,供 [GlobalClickWrapper] 及业务方读取。 /// /// 更新时机: /// - 路由 push → 更新为新路由 key /// - 路由 pop → 恢复为目标路由的 [_PageInfo.effectivePageKey](含子页面状态) /// - [recordNavigation] → Tab 切换或 PageView 翻页时手动更新 static String get currentPageKey => _currentPageKeyNotifier.value; /// 可监听的 currentPageKey,供业务方响应式监听路由变化。 /// /// 类型为 [ValueListenable](只读),防止外部直接写入破坏 SDK 内部状态。 /// 使用示例: /// ```dart /// PageLifecycleObserver.currentPageKeyListenable.addListener(() { /// print(PageLifecycleObserver.currentPageKey); /// }); /// ``` static ValueListenable get currentPageKeyListenable => _currentPageKeyNotifier; /// [_PageInfo.effectivePageKey],使 pop 返回时能恢复到正确的子页面 key。 /// /// **调用场景** /// - 由 [AnalyticsSdk.trackNavigation] 和 [AnalyticsSdk.updateCurrentPage] 统一调用; /// [BottomNavigationBar.withAnalytics] / [TabBar.withAnalytics] 内部已处理, /// 接入方通常不需要直接调用此方法。 /// /// 若堆栈为空(纯 Tab 导航、无路由层级),则只更新 [currentPageKey]。 static void recordNavigation(String pageKey) { _currentPageKeyNotifier.value = pageKey; if (_pageStack.isNotEmpty) { _pageStack.last.effectivePageKey = pageKey; } } static String _stripLeadingSlash(String name) => PageNameMapper.normalizeKey(name); /// 动态更新真实路由解析回调(支持 SDK re-init 时刷新) void setRealRouteResolver(String Function(String? actualRoute)? resolver) { _resolveRealRoute = resolver; } /// 解析路由名:优先使用注入的真实路由回调,失败时降级为 RouteSettings.name String? _resolveRouteName(Route? route) { if (route == null) return null; final actualRoute = route.settings.name; if (_resolveRealRoute == null) return actualRoute; try { final resolved = _resolveRealRoute?.call(actualRoute); if (resolved != null && resolved.isNotEmpty) return resolved; } catch (e) { Logger.pageLifecycleObserver(' 真实路由回调执行失败,降级使用 settings.name: $e'); } return actualRoute; } // 跟踪活动的 Timer,防止内存泄漏 final Map _activeTimers = {}; /// 本 observer 管辖的路由集合,用于 dispose 时精准清理, /// 避免多 Navigator 场景下影响其他 observer 的堆栈条目。 final Set _ownedRoutes = {}; PageLifecycleObserver({ required void Function(dynamic event) track, required String Function() getUserType, void Function(String pageKey)? onPageExit, String Function(String? actualRoute)? resolveRealRoute, this.autoTrackPageViews = true, }) : _track = track, _getUserType = getUserType, _onPageExit = onPageExit, _resolveRealRoute = resolveRealRoute; @override void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); try { if (route is PageRoute && route.settings.name != null) { final page = _ensurePageEntered(route); if (autoTrackPageViews) _schedulePageView(page); } } catch (e) { Logger.pageLifecycleObserver(' didPush() 异常,已安全处理: $e'); } } @override void didPop(Route route, Route? previousRoute) { super.didPop(route, previousRoute); try { _cancelPageViewTimer(route); if (_pageStack.isNotEmpty && _pageStack.last.route == route) { // 在出栈前先读取 prevEffectiveKey(push 时捕获的来路有效 key), // 避免依赖 _pageStack.last.effectivePageKey 在 pop 时仍正确—— // 后者可能因 recordNavigation 在 main 未入栈时调用而未被更新。 final restoreKey = _pageStack.last.prevEffectiveKey; _handlePageExit(route); _currentPageKeyNotifier.value = restoreKey.isNotEmpty ? restoreKey : _stripLeadingSlash(previousRoute?.settings.name ?? 'main'); } } catch (e) { Logger.pageLifecycleObserver(' didPop() 异常,已安全处理: $e'); } } @override void didRemove(Route route, Route? previousRoute) { super.didRemove(route, previousRoute); try { // pushAndRemoveUntil / removeRoute 走这里,需主动清理被移除路由 _cancelPageViewTimer(route); final removed = _pageStack.where((info) => info.route == route).toList(); _pageStack.removeWhere((info) => info.route == route); _ownedRoutes.remove(route); for (final info in removed) { SessionMetricsManager.instance.recordPageExit( pageKey: info.pageKey, pageName: info.pageName, enterTime: info.sessionEnterTime, exitTime: DateTime.now(), ); try { _onPageExit?.call(info.pageKey); } catch (e) { Logger.pageLifecycleObserver('onPageExit 回调异常 (didRemove): $e'); } } } catch (e) { Logger.pageLifecycleObserver(' didRemove() 异常,已安全处理: $e'); } } @override void didReplace({Route? newRoute, Route? oldRoute}) { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); try { // 旧页面 hide if (oldRoute != null) _cancelPageViewTimer(oldRoute); if (oldRoute is PageRoute && _pageStack.isNotEmpty) { if (_pageStack.last.route == oldRoute) { _handlePageExit(oldRoute); // 旧路由出栈后,将 currentPageKey 同步为新栈顶的 effectivePageKey。 // 不更新的话,_ensurePageEntered 捕获的 prevEffectiveKey 会是旧路由的 // key,导致新路由被 pop 时恢复到已不存在的旧路由,而非下层真实页面。 _currentPageKeyNotifier.value = _pageStack.isNotEmpty ? _pageStack.last.effectivePageKey : 'main'; } } // 新页面 show + 曝光 if (newRoute is PageRoute && newRoute.settings.name != null) { final page = _ensurePageEntered(newRoute); if (autoTrackPageViews) _schedulePageView(page); } } catch (e) { Logger.pageLifecycleObserver(' didReplace() 异常,已安全处理: $e'); } } /// 新 Session 开始且当前路由未变化时,从 Session 起点续开页面停留。 /// /// 该操作只写入 [SessionMetricsManager],不会调度或上报 [AppPageViewEvent]。 void startCurrentPageStayForNewSession(DateTime sessionStartedAt) { if (_pageStack.isEmpty) return; final current = _pageStack.last; current.sessionEnterTime = sessionStartedAt; SessionMetricsManager.instance.recordPageEnter( pageKey: current.pageKey, pageName: current.pageName, enterTime: sessionStartedAt, ); } /// 确保同一个 Flutter [Route] 只写入一次页面状态和 Session 停留明细。 _PageInfo _ensurePageEntered(PageRoute route) { for (final existing in _pageStack) { if (existing.route == route) return existing; } final rawKey = _stripLeadingSlash(_resolveRouteName(route) ?? route.settings.name!); final pageInfo = PageNameMapper.resolvePageInfo(rawKey); final pageKey = pageInfo.key; final pageName = pageInfo.name; // 在 currentPageKey 被覆盖前捕获:这是 pop 返回时应恢复的目标 key。 // 必须在 currentPageKey = pageKey 之前读取,否则读到的是新页面自身的 key。 final prevEffectiveKey = currentPageKey; _currentPageKeyNotifier.value = pageKey; final referrerKey = _pageStack.isEmpty ? '' : _pageStack.last.effectivePageKey; final referrerName = _pageStack.isEmpty ? '' : PageNameMapper.getPageName(_pageStack.last.effectivePageKey); final enterTime = DateTime.now(); // 页面堆栈容量检查 if (_pageStack.length >= _maxPageStackSize) { final evicted = _pageStack.removeAt(0); final evictedTimer = _activeTimers.remove(evicted.route); evictedTimer?.cancel(); SessionMetricsManager.instance.recordPageExit( pageKey: evicted.pageKey, pageName: evicted.pageName, enterTime: evicted.sessionEnterTime, exitTime: DateTime.now(), ); } final page = _PageInfo( pageKey: pageKey, pageName: pageName, routeEnterTime: enterTime, route: route, prevEffectiveKey: prevEffectiveKey, referrerPageKey: referrerKey, referrerPageName: referrerName, ); _pageStack.add(page); SessionMetricsManager.instance.recordPageEnter( pageKey: pageKey, pageName: pageName, enterTime: enterTime, ); _ownedRoutes.add(route); return page; } /// 在页面完成首帧渲染后自动上报一次 [AppPageViewEvent]。 void _schedulePageView(_PageInfo page) { if (page.pageViewScheduled) return; page.pageViewScheduled = true; final route = page.route; bool callbackExecuted = false; Timer? loadTimer; void frameCallback(Duration timestamp) { if (callbackExecuted) return; // 页面可能在帧回调等待期间被 pop,检查是否仍在栈中。 final stillOnStack = _pageStack.any((info) => info.route == route); if (!stillOnStack) { callbackExecuted = true; loadTimer?.cancel(); _activeTimers.remove(route); return; } callbackExecuted = true; final pageLoadTime = DateTime.now().difference(page.routeEnterTime).inMilliseconds; loadTimer?.cancel(); _activeTimers.remove(route); String userType; try { userType = _getUserType(); } catch (e) { Logger.pageLifecycleObserver(' 获取用户类型失败,使用默认值: $e'); userType = 'normal'; } try { _track(AppPageViewEvent( userType: userType, pageKey: page.pageKey, pageName: page.pageName, referrerPageKey: page.referrerPageKey, referrerPageName: page.referrerPageName, currentPageKey: page.pageKey, currentPageName: page.pageName, pageLoadTime: pageLoadTime, pageEnterTs: page.routeEnterTime.millisecondsSinceEpoch ~/ 1000, )); } catch (e) { Logger.pageLifecycleObserver(' AppPageViewEvent 上报失败: $e'); } } // 超时保护:如果第一帧回调未在规定时间内执行,重新注册。 loadTimer = Timer(SdkConfig.pageLoadTimeout, () { if (!callbackExecuted && _pageStack.isNotEmpty && _pageStack.last.route == route) { SchedulerBinding.instance.addPostFrameCallback(frameCallback); } }); _activeTimers[route] = loadTimer; SchedulerBinding.instance.addPostFrameCallback(frameCallback); } /// 释放所有待触发的 Timer,防止 dispose 后继续触发幽灵上报。 /// 只移除本 observer 管辖的路由条目,避免多 Navigator 场景下清空其他 observer 的堆栈。 void dispose() { for (final timer in _activeTimers.values) { timer.cancel(); } _pageStack.removeWhere((info) => _ownedRoutes.contains(info.route)); _activeTimers.clear(); _ownedRoutes.clear(); } void _cancelPageViewTimer(Route route) { final timer = _activeTimers.remove(route); timer?.cancel(); } /// 处理页面退出:出栈、取消对应 Timer、触发 [_onPageExit] 回调。 void _handlePageExit(Route route) { if (_pageStack.isEmpty || _pageStack.last.route != route) return; final exitingPage = _pageStack.last; final exitingPageKey = exitingPage.pageKey; _pageStack.removeLast(); _ownedRoutes.remove(route); SessionMetricsManager.instance.recordPageExit( pageKey: exitingPage.pageKey, pageName: exitingPage.pageName, enterTime: exitingPage.sessionEnterTime, exitTime: DateTime.now(), ); // 清理该路由的 Timer,防止内存泄漏 _cancelPageViewTimer(route); try { _onPageExit?.call(exitingPageKey); } catch (e) { Logger.pageLifecycleObserver('onPageExit 回调异常: $e'); } } } /// 路由页面快照,记录页面进入时的状态。 /// /// [effectivePageKey] 是唯一可变字段:初始值与 [pageKey] 相同, /// 当该路由位于栈顶时,内部 Tab / PageView 切换会通过 /// [PageLifecycleObserver.recordNavigation] 将其更新为子页面 key; /// 新路由 push 的 referrer 均能精准反映子页面位置。 /// /// [prevEffectiveKey] 在 push 时捕获彼时的 [PageLifecycleObserver.currentPageKey], /// pop 返回时用于恢复 [PageLifecycleObserver.currentPageKey]。 /// 与 [effectivePageKey] 的区别:前者存储在被 push 的新页面里, /// 后者存储在上一层页面里——当上层页面是"容器路由"时, /// effectivePageKey 可能因时序问题未能同步 tab 状态, /// 而 prevEffectiveKey 在 push 时即时捕获,始终准确。 class _PageInfo { /// 路由自身的页面 key(由路由名称规范化而来,不可变)。 final String pageKey; /// 路由自身的展示名称(由 [PageNameMapper] 解析,不可变)。 final String pageName; /// 该路由当前的有效页面 key。 /// /// 初始等于 [pageKey];路由处于栈顶时,内部子页面切换会将其更新为子页面 key, /// 从而使新路由 push 的 referrer 能精准反映子页面位置。 String effectivePageKey; /// push 此路由时捕获的前一页有效 key,pop 时用于恢复 [PageLifecycleObserver.currentPageKey]。 /// /// 在 [PageLifecycleObserver._ensurePageEntered] 的第一行读取 /// [PageLifecycleObserver.currentPageKey](早于 `currentPageKey = pageKey`), /// 保证容器路由 + tab 场景下返回时能恢复到正确的子页面 key 而非容器路由 key。 final String prevEffectiveKey; /// 进入该路由时的来路页面信息,供自动页面曝光使用。 final String referrerPageKey; final String referrerPageName; /// 防止 `pageObserver` 与 `sessionObserver` 误同时注册时重复自动曝光。 bool pageViewScheduled = false; /// 路由首次进入时刻,只用于页面曝光时间和 pageLoadTime。 final DateTime routeEnterTime; /// 当前 Session 内的页面进入时刻,用于页面停留统计。 DateTime sessionEnterTime; /// 对应的 Flutter 路由对象,用于 Timer / 堆栈查找时的精确匹配。 final Route route; _PageInfo({ required this.pageKey, required this.pageName, required this.routeEnterTime, required this.route, required this.prevEffectiveKey, required this.referrerPageKey, required this.referrerPageName, }) : effectivePageKey = pageKey, sessionEnterTime = routeEnterTime; }