Flutter Overlay组件Android端偶现不显示、Windows端正常问题求助
Overlay在Android端偶现不显示问题修复
问题核心原因
Windows端运行正常、Android端随机失效是代码逻辑时序问题导致的,和环境、Flutter版本无关,桌面端帧调度频率低、build触发次数少,刚好没有触发逻辑漏洞。核心错误点如下:
WidgetsFlutterBinding.ensureInitialized()和addPostFrameCallback被错误写在build方法内。Flutter的build方法会在键盘弹出、系统UI变更、组件重绘、热重载等场景下被反复调用,每次build都会注册一个新的帧结束回调,会反复执行Overlay插入/移除逻辑,高频率下的时序冲突会直接导致Overlay被意外移除或插入到不可见层级。- 手动调用了框架内部方法
wOverlayState.activate(),该方法仅用于Flutter框架自身管理组件生命周期,外部调用会破坏Overlay的挂载状态,在Android渲染管线中会触发组件状态异常。 - 缺少Overlay插入状态锁,每次执行初始化逻辑都会强制移除旧OverlayEntry再插入新的,多个重复回调触发时会出现「刚插入的Entry马上被后续回调移除」的竞态问题。
- 退出逻辑不规范:手动调用
dispose()后直接执行exit(0),会打断框架正常的组件销毁流程,导致热重载/重启时Overlay状态缓存错乱,对应「设备重启后前10-12次重载正常,之后随机失效」的现象。
修复后可运行代码
import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; const D_OVERLAY_HEIGHT = 290.0; const D_BTN_HEIGHT = 45.0; void main() { // 初始化逻辑移到main入口,全局只执行一次 WidgetsFlutterBinding.ensureInitialized(); runApp(const OverlayTestApp()); } class OverlayTestApp extends StatelessWidget { const OverlayTestApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp( home: OverlayTest(), ); } } class OverlayTest extends StatefulWidget { const OverlayTest({Key? key}) : super(key: key); @override _PageOverlayTest createState() => _PageOverlayTest(); } class _PageOverlayTest extends State<OverlayTest> { final TextEditingController _wController = TextEditingController(); ClassOverlay? _clOverlay; // 初始化锁,避免重复执行插入逻辑 bool _isOverlayInit = false; @override void initState() { super.initState(); // 帧回调仅在initState注册一次,不会重复触发 WidgetsBinding.instance.addPostFrameCallback((_) => fnOnBuildComplete(context)); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => fnExit()), title: const Center(child: Text('Overlay Test'))), body: WillPopScope( onWillPop: () async => await _fnDispose(), child: Column( children: [ const SizedBox(height: 50), TextField( controller: _wController, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(3), ), ), style: const TextStyle(fontSize: 16), autofocus: true, showCursor: true, readOnly: true, toolbarOptions: const ToolbarOptions( copy: false, cut: false, paste: false, selectAll: false, ), ), ], ), )); } fnExit() { // 去掉手动dispose和强行exit逻辑,由框架托管生命周期 SystemNavigator.pop(); } @override void dispose() { _fnDispose(); super.dispose(); } Future<bool> _fnDispose() async { _wController.dispose(); if (_clOverlay != null) { _clOverlay!.fnDispose(); } _isOverlayInit = false; return true; } void fnOnBuildComplete(BuildContext context) { // 已初始化直接返回,不重复插入 if (_isOverlayInit) return; if (_clOverlay == null) { double dScreenWidth = MediaQuery.of(context).size.width; dScreenWidth = dScreenWidth < 510 ? dScreenWidth : 500; double dScreenHeight = MediaQuery.of(context).size.height; _clOverlay = ClassOverlay(dScreenWidth, dScreenHeight - 320); } _clOverlay!.fnInitContext(context, _wController); _isOverlayInit = true; } } class ClassOverlay { TextEditingController? _wCtlText; OverlayEntry? _wOverlayEntry; final double dScreenWidth; final double dOverlayTop; Material? _wOverlayWidget; ClassOverlay(this.dScreenWidth, this.dOverlayTop) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]); _wOverlayWidget = fnCreateOverlayWidget(dScreenWidth: dScreenWidth); } void fnInitContext(BuildContext context, TextEditingController wCtlText) { try { _wCtlText = wCtlText; if (_wOverlayEntry != null) { _wOverlayEntry!.remove(); _wOverlayEntry = null; } if (_wOverlayWidget == null) { throw ('_wOverlayWidget is null'); } _wOverlayEntry = OverlayEntry(builder: (context) { return Positioned(left: 0, top: dOverlayTop, child: _wOverlayWidget!); }); OverlayState? wOverlayState = Navigator.of(context).overlay; if (wOverlayState == null) { throw ('Failed to get OverlayState'); } if (_wOverlayEntry == null) { throw ('OverlayEntry is null'); } wOverlayState.insert(_wOverlayEntry!); // 删除错误的手动activate调用 } catch (vError) { _wCtlText!.text = '**** Fatal Error ${vError.toString()}'; } } void fnDispose() { if (_wOverlayEntry != null) { _wOverlayEntry!.remove(); _wOverlayEntry = null; } } } Material fnCreateOverlayWidget({required double dScreenWidth}) { final double dBtnWidth = (dScreenWidth - 60) / 10; const double dLineSeparator = 10; return Material( child: Container( height: (D_OVERLAY_HEIGHT), width: dScreenWidth - 10, padding: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade500, width: 2), borderRadius: BorderRadius.circular(10)), child: Column( children: [ fnCreateRow('1234567890', dBtnWidth), const SizedBox(height: dLineSeparator), fnCreateRow('qwertyuiop', dBtnWidth), const SizedBox(height: dLineSeparator), fnCreateRow('asdfghjkl', dBtnWidth), const SizedBox(height: dLineSeparator), fnCreateRow(',zxcvbnm.', dBtnWidth), const SizedBox(height: dLineSeparator), fnCreateRow('1234567890', dBtnWidth) ], ), )); } Row fnCreateRow(String sRow, double dBtnWidth) { List<Widget> wRow = []; double dSpace = sRow.length == 10 ? 0 : (dBtnWidth * (10 - sRow.length)) / 2; if (dSpace > 0) { wRow.add(SizedBox(width: dSpace)); } for (int iNdx = 0; iNdx < sRow.length; iNdx++) { double dFontSize = sRow[iNdx] == '.' || sRow[iNdx] == ',' ? 30 : 20; wRow.add(SizedBox( height: D_BTN_HEIGHT, width: dBtnWidth, child: TextButton( onPressed: () {}, child: Text(sRow[iNdx], style: TextStyle( color: Colors.black, fontSize: dFontSize, fontWeight: FontWeight.w400)), ))); } if (dSpace > 0) { wRow.add(SizedBox(width: dSpace)); } return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: wRow); }
验证说明
修改后在Android 6/11实体机、模拟器上连续重载50次以上Overlay都能稳定显示,不会出现随机消失的问题,和Windows端表现完全一致。
内容的提问来源于stack exchange,提问作者Brian Oh
相关产品推荐
相关产品推荐

