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的显示/隐藏:
- 修改焦点监听逻辑,仅在获取焦点且Overlay未创建时插入Overlay;
- 给Overlay添加全局点击监听,点击外部区域时移除Overlay并取消焦点;
- 在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
相关产品推荐
相关产品推荐

