You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter如何实现可见组件每5秒更新 解决Timer泄漏、可见性检测问题

Flutter 可见时轮询更新观看人数组件实现方案

现有问题根因

  • Timer 泄漏问题:
    1. dispose 方法错误添加 async 关键字,导致Timer取消逻辑延迟执行,单元测试检测到组件销毁后仍有未释放的Timer
    2. Timer回调触发前没有校验组件挂载状态,组件销毁后依然会执行网络请求
    3. 单次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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 11:24:41