Files
hjsq_android/analytics_sdk/lib/observer/page_lifecycle_observer.dart
T
2026-10-08 11:37:25 +07:00

459 lines
18 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.
// 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<String> _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<String> 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<Route, Timer> _activeTimers = <Route, Timer>{};
/// 本 observer 管辖的路由集合,用于 dispose 时精准清理,
/// 避免多 Navigator 场景下影响其他 observer 的堆栈条目。
final Set<Route> _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;
}