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

Flutter Web中使用Overlay时ListTile的onTap回调失效问题

Flutter Web中Overlay内ListTile的onTap无响应问题

我在Flutter Web上实现了一个自定义下拉组件:当TextField获取焦点时,通过Overlay在其下方渲染ListView作为下拉菜单,但ListView中的ListTile点击后onTap回调完全没有反应。

问题代码

import 'package:flutter/material.dart';

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {
  final FocusNode _focusNode = FocusNode();

  OverlayEntry? _overlayEntry;

  List<String> countries = ['Lebanon', 'Syria', 'India'];

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {
        _overlayEntry = _createOverlayEntry();
        Overlay.of(context).insert(_overlayEntry!);
      } else {
        _overlayEntry!.remove();
      }
    });
  }

  OverlayEntry _createOverlayEntry() {
    RenderBox? renderBox = context.findRenderObject() as RenderBox?;
    var size = renderBox?.size;
    var offset = renderBox?.localToGlobal(Offset(0, size!.height + 5.0));

    return OverlayEntry(
      builder: (context) => Positioned(
        left: offset?.dx,
        top: offset?.dy,
        width: size?.width,
        child: Material(
          elevation: 4.0,
          child: SizedBox(
            width: 200,
            height: 300,
            child: ListView.builder(
              padding: EdgeInsets.zero,
              itemCount: countries.length,
              itemBuilder: (BuildContext context, index) {
                return ListTile(
                  title: Text(countries[index]),
                  onTap: () {
                    print(countries[index]);
                  },
                );
              },
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _focusNode.dispose();
    _overlayEntry?.remove();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      focusNode: _focusNode,
      decoration: InputDecoration(labelText: 'Country'),
    );
  }
}

问题原因

核心问题在于焦点触发的时机冲突:当点击ListTile时,系统会先将焦点从TextField移除,触发_focusNode的监听回调直接移除Overlay组件。此时ListTile的onTap回调还未执行,就已经随着Overlay被销毁了。

解决方法

调整逻辑,不再依赖焦点失焦事件直接移除Overlay,而是通过点击ListTile或外部区域主动控制Overlay的显示/隐藏:

  1. 修改焦点监听逻辑,仅在获取焦点且Overlay未创建时插入Overlay;
  2. 给Overlay添加全局点击监听,点击外部区域时移除Overlay并取消焦点;
  3. 在ListTile的onTap回调中先执行业务逻辑,再移除Overlay并取消焦点。

修改后的完整代码

import 'package:flutter/material.dart';

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {
  final FocusNode _focusNode = FocusNode();
  OverlayEntry? _overlayEntry;
  List<String> countries = ['Lebanon', 'Syria', 'India'];

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(() {
      if (_focusNode.hasFocus && _overlayEntry == null) {
        // 仅在获取焦点且Overlay未创建时插入
        _overlayEntry = _createOverlayEntry();
        Overlay.of(context).insert(_overlayEntry!);
      }
    });
  }

  OverlayEntry _createOverlayEntry() {
    final RenderBox renderBox = context.findRenderObject() as RenderBox;
    final size = renderBox.size;
    final offset = renderBox.localToGlobal(Offset(0, size.height + 5.0));

    return OverlayEntry(
      builder: (context) => GestureDetector(
        // 点击外部区域时关闭Overlay并取消焦点
        onTap: () {
          _closeOverlay();
        },
        child: Stack(
          children: [
            // 透明占位层,确保外部点击能被捕获
            Positioned.fill(child: Container(color: Colors.transparent)),
            Positioned(
              left: offset.dx,
              top: offset.dy,
              width: size.width,
              child: Material(
                elevation: 4.0,
                child: SizedBox(
                  width: 200,
                  height: 300,
                  child: ListView.builder(
                    padding: EdgeInsets.zero,
                    itemCount: countries.length,
                    itemBuilder: (BuildContext context, index) {
                      return ListTile(
                        title: Text(countries[index]),
                        onTap: () {
                          // 先执行业务逻辑
                          print(countries[index]);
                          // 再关闭Overlay并取消焦点
                          _closeOverlay();
                        },
                      );
                    },
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  // 封装关闭Overlay的逻辑
  void _closeOverlay() {
    _focusNode.unfocus();
    _overlayEntry?.remove();
    _overlayEntry = null;
  }

  @override
  void dispose() {
    _focusNode.dispose();
    _overlayEntry?.remove();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      focusNode: _focusNode,
      decoration: InputDecoration(labelText: 'Country'),
    );
  }
}

内容的提问来源于stack exchange,提问作者Anorakk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 00:25:41