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

Flutter Web输入框获焦时Hover返回按钮触发错误的解决咨询

问题描述

我在开发一个小型Flutter Web应用,页面采用Stack布局,包含表单与返回上一页按钮。当输入框处于获焦状态,鼠标hover返回按钮时,会触发如下错误:

errors.dart:296 Uncaught Error
at Object.throw_ [as throw] (errors.dart:296:3)
at Object.assertFailed (errors.dart:29:3)
at Object._computeOffsetForInputs (event_position_helper.dart:64:37)
at Object.computeEventOffsetToTarget (event_position_helper.dart:32:14)
at [_convertEventsToPointerData] (pointer_binding.dart:1037:30)
at pointer_binding.dart:976:9
at pointer_binding.dart:930:7
at loggedHandler (pointer_binding.dart:531:9)
at Object._checkAndCall (operations.dart:426:37)
at Object.dcall (operations.dart:431:39)
at ret (js_allow_interop_patch.dart:17:11)

输入框未获焦时运行正常,求修复方案。

环境信息

  • Flutter version 3.22.0
  • Dart version 3.4.0
  • DevTools version 2.34.3

核心代码

ResetPasswordView 代码

class ResetPasswordView extends StatelessWidget {

  const ResetPasswordView({
    super.key,
  });

  Widget formSection(BuildContext context, ResetPasswordViewModel viewModel) {
    final AppLocalizations l10n = AppLocalizations.of(context);
    return SingleChildScrollView(
      // TODO Max : A bouger dans le layout
      physics: const BouncingScrollPhysics(),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          AppSpacer.p64(),
          // TODO Replace by image
          if (MediaQuery.of(context).size.width > 650)
            const Placeholder(fallbackHeight: 100),
          AppSpacer.p8(),
          Form(
            key: viewModel.formKey,
            child: FocusScope(
              node: viewModel.focusScopeNode,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  InputEmail(
                    controller: viewModel.emailController,
                    autofocus: true,
                    enabled: !viewModel.isBusy,
                    onChange: viewModel.onFormChange,
                    onEditingComplete: () async => viewModel.onEditingCompleted(
                        context: context, submit: true),
                  ),
                  if (viewModel.busy(viewModel.isResetPasswordButtonEnabled))
                    const CircularProgressIndicator()
                  else
                    SizedBox(
                      width: double.infinity,
                      child: Padding(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 12.0, vertical: 8.0),
                        child: FilledButton(
                          onPressed: viewModel.isResetPasswordButtonEnabled
                              ? () async => viewModel.resetPassword(context)
                              : null,
                          child: Text(
                            l10n.reset_password_view_reset_password,
                            style: const TextStyle(fontSize: 20.0),
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final AppLocalizations l10n = AppLocalizations.of(context);
    final GoRouter router = GoRouter.of(context);
    return ViewModelBuilder<ResetPasswordViewModel>.reactive(
      viewModelBuilder: () => ResetPasswordViewModel(router, l10n),
      builder: (context, viewModel, _) => AppScaffold(
        body: AuthenticationLayout(
          widget: formSection(context, viewModel),
          isBackArrow: true,
        ),
      ),
    );
  }
}

AuthenticationLayout 代码

class AuthenticationLayout extends StatelessWidget {
  final Widget widget;
  final bool isBackArrow;

  const AuthenticationLayout({
    required this.widget,
    this.isBackArrow = false,
    super.key,
  });

  Widget rightForm(Widget widget, double screenHeight, double width) {
    return new Container(
      width: width,
      height: screenHeight,
      decoration: new BoxDecoration(color: Color.fromRGBO(12, 71, 28, 1)),
      // TODO Max : A changer et à sortir dans le thème
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: 64),
        child: widget,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final MediaQueryData m = MediaQuery.of(context);
    return Stack(
      children: [
        rightForm(widget, m.size.height, m.size.width),
        if (isBackArrow)
          InkWell(
              onHover: (value) {
                print(value);
              },
              onTap: () => GoRouter.of(context).pop(),
              child: Icon(Icons.arrow_back_rounded))
      ],
    );
  }
}

解决方案

这个问题是Flutter Web在处理焦点元素与Stack上层hover交互时的已知布局/事件计算bug,核心原因是InkWell的hover事件在计算偏移时,与下层获焦的输入框元素出现坐标冲突。以下是几种可行的修复方案:

方案1:给返回按钮添加定位与内边距

在AuthenticationLayout的InkWell外层包裹Positioned明确按钮位置,同时给Icon添加内边距,避免hover事件触发时的坐标计算异常:

if (isBackArrow)
  Positioned(
    top: 20,
    left: 20,
    child: InkWell(
      onHover: (value) {
        print(value);
      },
      onTap: () => GoRouter.of(context).pop(),
      child: const Padding(
        padding: EdgeInsets.all(8.0),
        child: Icon(Icons.arrow_back_rounded),
      ),
    ),
  )

方案2:调整焦点控制逻辑

原代码中InputEmail设置了autofocus: true且嵌套在FocusScope下,易导致Web端焦点绑定异常。移除自动聚焦,改为在ViewModel初始化时手动请求焦点:
在ResetPasswordViewModel中添加初始化逻辑:

class ResetPasswordViewModel extends ReactiveViewModel {
  // ... 原有代码
  final FocusScopeNode focusScopeNode = FocusScopeNode();
  final TextEditingController emailController = TextEditingController();

  @override
  void onInit() {
    super.onInit();
    // 延迟请求焦点,避免布局未完成时触发异常
    Future.delayed(const Duration(milliseconds: 100), () {
      focusScopeNode.requestFocus();
    });
  }

  // ... 原有代码
}

同时将InputEmail的autofocus: true改为autofocus: false。

方案3:替换InkWell为GestureDetector

InkWell在Web端的hover事件实现存在兼容性问题,改用GestureDetector的onEnter/onExit替代hover逻辑:

if (isBackArrow)
  Positioned(
    top: 20,
    left: 20,
    child: GestureDetector(
      onTap: () => GoRouter.of(context).pop(),
      onEnter: (_) => print(true),
      onExit: (_) => print(false),
      child: const Padding(
        padding: EdgeInsets.all(8.0),
        child: Icon(Icons.arrow_back_rounded),
      ),
    ),
  )

方案4:升级Flutter版本

该bug在Flutter 3.24+稳定版中已被官方修复,若环境允许,直接执行升级命令即可解决:

flutter upgrade

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 18:45:10