Initial commit: 91dj android client dev_1.0.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-08 11:37:25 +07:00
co-authored by Claude Opus 5.5
commit 97dd6a739a
1262 changed files with 119813 additions and 0 deletions
@@ -0,0 +1,458 @@
// 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;
}