Flutter如何实现可见组件每5秒更新 解决Timer泄漏、可见性检测问题
Flutter 可见时轮询更新观看人数组件实现方案
现有问题根因
- Timer 泄漏问题:
dispose方法错误添加async关键字,导致Timer取消逻辑延迟执行,单元测试检测到组件销毁后仍有未释放的Timer- Timer回调触发前没有校验组件挂载状态,组件销毁后依然会执行网络请求
- 单次Timer没有做递归调度,容易出现定时器堆叠、生命周期绑定失效
- 可见性检测失效问题:
visibility_detector仅基于组件在视口内的绘制占比判断可见性,模态弹窗属于上层路由覆盖,下层组件依然正常绘制占满视口,因此不会触发可见性变化回调。
正确实现方案
1. 修复Timer生命周期管理问题
放弃固定周期Timer,改用单次Timer递归调度,全链路加挂载状态校验,dispose同步执行取消逻辑:
Timer? _viewerTimer; int _viewerCount = 0; // 调度下一次轮询 void _scheduleNextFetch() { _viewerTimer?.cancel(); // 组件已销毁直接终止调度 if (!mounted) return; _viewerTimer = Timer(const Duration(seconds: 5), () async { // 回调触发第一校验:组件已销毁/不可见直接终止 if (!mounted || !_isFullVisible) return; try { // 替换为实际接口请求逻辑 final res = await fetchCurrentViewerCount(); // 请求返回后二次校验挂载状态,避免请求过程中组件销毁 if (!mounted) return; setState(() { _viewerCount = res; }); } catch (e) { // 按需处理请求异常 } finally { // 请求完成后再调度下一次,避免接口慢导致请求堆叠 _scheduleNextFetch(); } }); } @override void dispose() { // dispose必须为同步方法,不能加async _viewerTimer?.cancel(); _viewerTimer = null; super.dispose(); }
2. 解决模态窗遮挡下的可见性判断问题
结合RouteObserver路由生命周期监听+visibility_detector视口可见性双重判断,覆盖所有不可见场景:
- 第一步:在应用根节点注册全局路由观察者
// 全局路由观察者实例 final RouteObserver<ModalRoute> appRouteObserver = RouteObserver<ModalRoute>(); void main() { runApp(MaterialApp( navigatorObservers: [appRouteObserver], // 其他应用配置 )); }
- 第二步:在组件中混合路由监听,结合两种可见性维度做判断
class ViewerCountWidget extends StatefulWidget { const ViewerCountWidget({super.key}); @override State<ViewerCountWidget> createState() => _ViewerCountWidgetState(); } class _ViewerCountWidgetState extends State<ViewerCountWidget> with RouteAware { // 路由是否处于栈顶(未被弹窗、新页面覆盖) bool _isRouteOnTop = true; // 组件是否在当前页面视口内(未被滚动遮挡) bool _isInViewPort = true; Timer? _viewerTimer; int _viewerCount = 0; // 两个条件同时满足才算真正可见 bool get _isFullVisible => _isRouteOnTop && _isInViewPort; @override void didChangeDependencies() { super.didChangeDependencies(); // 订阅路由生命周期事件 appRouteObserver.subscribe(this, ModalRoute.of(context)!); } // 有新路由入栈,当前页面被覆盖 @override void didPushNext() { _isRouteOnTop = false; _onVisibleChange(); super.didPushNext(); } // 上层路由出栈,当前页面回到栈顶 @override void didPopNext() { _isRouteOnTop = true; _onVisibleChange(); super.didPopNext(); } // 统一处理可见性变化 void _onVisibleChange() { if (_isFullVisible) { // 变为可见时立刻拉取一次数据,无需等待5秒间隔 _fetchImmediatelyAndSchedule(); } else { // 不可见时直接取消定时器 _viewerTimer?.cancel(); } } void _fetchImmediatelyAndSchedule() { _viewerTimer?.cancel(); // 立刻触发首次请求 _viewerTimer = Timer(Duration.zero, () => _scheduleNextFetch()); } // 处理视口可见性变化 void _handleViewPortChange(VisibilityInfo info) { _isInViewPort = info.visibleFraction > 0; _onVisibleChange(); } @override Widget build(BuildContext context) { return VisibilityDetector( key: const ValueKey('viewer_count_widget'), onVisibilityChanged: _handleViewPortChange, child: Text('当前有$_viewerCount位用户正在观看'), ); } }
关键注意点
- 所有异步回调、Timer回调执行前必须校验
mounted属性,禁止操作已销毁的组件 - 轮询间隔从请求完成后开始计算,避免接口耗时导致请求堆积
dispose为同步生命周期方法,禁止添加async关键字,否则会导致资源释放时序异常- 该方案可覆盖页面切后台、弹出模态窗、组件滚动出视口、页面销毁等所有不可见场景,不会出现后台偷偷发请求的问题
内容的提问来源于stack exchange,提问作者anber
相关产品推荐
相关产品推荐

