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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 07:45:30