import 'package:flutter/widgets.dart'; import 'package:uuid/uuid.dart'; /// sessionId 生命周期管理(13 号规范 §3.1、06 号报告 §5.1)。 /// /// 语义三条:冷启动生成新 sessionId;`paused → resumed` 间隔超过 /// [timeout](默认 30 分钟)生成新 sessionId;未超过则沿用原值。 /// sessionId 不落任何持久化存储——会话本该跨冷启动失效(03 号评估 §3.1, /// 纯内存方案,lastActiveAt 不持久化)。 /// /// 注意生命周期级联:前台恢复时状态机会依次经过 /// `paused → hidden → inactive → resumed`,因此只在**离开 resumed 的第一次 /// 变更**记录退后台时刻,后续级联状态不得覆盖,否则间隔永远趋近于零。 class SessionTracker with WidgetsBindingObserver { SessionTracker({ this.timeout = const Duration(minutes: 30), this.onLeaveForeground, DateTime Function()? now, }) : _now = now ?? DateTime.now, _sessionId = const Uuid().v7(); /// 后台超时阈值;构造参数化便于测试注入。 final Duration timeout; /// 首次离开前台时回调(app 装配层用于触发埋点队列冲刷)。 final VoidCallback? onLeaveForeground; /// 时钟注入口,测试免真实等待。 final DateTime Function() _now; String _sessionId; DateTime? _leftForegroundAt; AppLifecycleState _lastState = AppLifecycleState.resumed; /// 当前会话标识(UUIDv7)。 String get sessionId => _sessionId; @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { final leftAt = _leftForegroundAt; if (leftAt != null && _now().difference(leftAt) > timeout) { _sessionId = const Uuid().v7(); } _leftForegroundAt = null; } else if (_lastState == AppLifecycleState.resumed) { // 首次离开前台才记时;inactive/hidden/paused 级联不覆盖。 _leftForegroundAt = _now(); onLeaveForeground?.call(); } _lastState = state; } }