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

Flutter Web点击首页按钮报'Scheduling update'焦点错误求助

Web端Flutter按钮点击抛出焦点错误排查

问题现象

Web端运行Flutter应用时,点击首页任意按钮都会抛出错误:Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus,但移动端和桌面端运行完全正常,且未手动使用任何focus()实例或相关焦点操作。

相关代码

HomeScreen 组件

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return ScaffoldMessenger(
      child: Scaffold(
        drawer: const MyDrawer(),
        appBar: AppBar(
          title: const Text("Tela inicial"),
          actions: const [
            IconSwitchTheme(),
          ],
          titleTextStyle: TextStyle(
              fontSize: MediaQuery.of(context).size.width > 400
                  ? MediaQuery.of(context).size.width > 700
                      ? 40.0
                      : 24.0
                  : 19.0),
        ),
        body: Center(
          child: SizedBox(
            height: MediaQuery.of(context).size.height,
            width: MediaQuery.of(context).size.width * 0.9,
            child: Column(
              children: [
                SizedBox(
                  height: MediaQuery.of(context).size.height < 700
                      ? MediaQuery.of(context).size.height * 0.15
                      : MediaQuery.of(context).size.height * 0.2,
                  width: double.infinity,
                  child: Image.asset("assets/images/labsec-logo.png"),
                ),
                Center(
                  child: Text(
                    "Desafio App",
                    style: MediaQuery.of(context).size.height < 700
                        ? Theme.of(context).textTheme.bodyLarge
                        : Theme.of(context)
                            .textTheme
                            .bodyLarge!
                            .copyWith(fontSize: 50.0),
                  ),
                ),
                const SizedBox(height: 30),
                Expanded(
                  child: SizedBox(
                    width: MediaQuery.of(context).size.width > 700
                        ? MediaQuery.of(context).size.width * 0.45
                        : MediaQuery.of(context).size.width * 0.85,
                    child: GridView.count(
                      padding: const EdgeInsets.all(8.0),
                      shrinkWrap: true,
                      // physics: const NeverScrollableScrollPhysics(),
                      crossAxisCount: 2,
                      mainAxisSpacing: 15.0,
                      crossAxisSpacing: 15.0,
                      children: [
                        CustomButton(
                          onPressed: () => Navigator.of(context)
                              .pushNamed("/bluetooth-devices"),
                          child: const HomeScreenButtonChild(
                            icon: Icons.bluetooth,
                            text: "Dispositivos LBE",
                          ),
                        ),
                        CustomButton(
                          onPressed: () =>
                              Navigator.of(context).pushNamed('/rsa-keys'),
                          child: const HomeScreenButtonChild(
                            icon: Icons.key,
                            text: "Gerar chaves RSA",
                          ),
                        ),
                        CustomButton(
                          onPressed: () =>
                              Navigator.of(context).pushNamed('/sign-list'),
                          child: const HomeScreenButtonChild(
                            icon: Icons.edit_note,
                            text: "Assinar lista",
                          ),
                        ),
                        CustomButton(
                          onPressed: () => Navigator.of(context)
                              .pushNamed('/verify-signature'),
                          child: const HomeScreenButtonChild(
                            icon: Icons.search,
                            text: "Verificar assinatura",
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 20),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

CustomButton 组件

class CustomButton extends StatelessWidget {
  const CustomButton({
    super.key,
    required this.onPressed,
    required this.child,
    this.style,
    this.spinner,
  });

  final VoidCallback? onPressed;
  final ButtonStyle? style;
  final bool? spinner;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      style: style,
      child: spinner != null ? const CircularProgressIndicator() : child,
    );
  }
}

HomeScreenButtonChild 组件

class HomeScreenButtonChild extends StatelessWidget {
  const HomeScreenButtonChild({
    super.key,
    required this.icon,
    required this.text,
  });

  final IconData icon;
  final String text;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const SizedBox(height: 10.0),
        Expanded(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Icon(icon,
                  size: MediaQuery.of(context).size.width > 700
                      ? MediaQuery.of(context).size.width > 1000
                          ? 120.0
                          : 80.0
                      : 40.0),
            ],
          ),
        ),
        const SizedBox(height: 10.0),
        Expanded(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Flexible(
                child: Text(
                  text,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                      fontSize: MediaQuery.of(context).size.width > 700
                          ? MediaQuery.of(context).size.width > 1000
                              ? 40.0
                              : 20.0
                          : 16.0),
                ),
              ),
            ],
          ),
        )
      ],
    );
  }
}

MyDrawer 组件

class MyDrawer extends StatelessWidget {
  const MyDrawer({super.key});

  @override
  Widget build(BuildContext context) {
    final ThemeMode themeMode = context.watch<ThemeProvider>().themeMode;

    return Drawer(
      backgroundColor: themeMode == ThemeMode.light
          ? Colors.white
          : Theme.of(context).colorScheme.primary,
      child: ListView(
        children: [
          DrawerHeader(
            child: Center(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Text(
                    "CONFIGURAÇÕES",
                    style: Theme.of(context).textTheme.titleLarge!.copyWith(
                          color: themeMode == ThemeMode.light
                              ? Theme.of(context).colorScheme.primary
                              : Colors.white,
                        ),
                  ),
                  IconButton(
                    icon: const Icon(Icons.close),
                    iconSize: 30,
                    color: themeMode == ThemeMode.light
                        ? Theme.of(context).colorScheme.primary
                        : Colors.white,
                    onPressed: () => Navigator.of(context).pop(),
                  )
                ],
              ),
            ),
          ),
          // const Divider(),
          ListTile(
            leading: const Icon(
              Icons.delete,
              color: Colors.red,
            ),
            title: Text(
              "Deletar todos os dados",
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            onTap: () {
              context.read<BluetoothDevicesProvider>().clearProvider();
              context.read<RSAKeysProvider>().clearProvider();
              context.read<SignatureProvider>().clearProvider();
            },
          )
        ],
      ),
    );
  }
}

环境配置

[✓] Flutter (Channel stable, 3.7.3, on macOS 13.2.1 22D68 darwin-arm64, locale en-US)
    • Flutter version 3.7.3 on channel stable at /opt/homebrew/Caskroom/flutter/3.7.3/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 9944297138 (2 weeks ago), 2023-02-08 15:46:04 -0800
    • Engine revision 248290d6d5
    • Dart version 2.19.2
    • DevTools version 2.20.1

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
    • Android SDK at /Users/enrico/Library/Android/sdk
    • Platform android-33, build-tools 33.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14C18
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)

[✓] VS Code (version 1.75.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.58.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-arm64   • macOS 13.2.1 22D68 darwin-arm64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 110.0.5481.100

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

原因分析

这个错误是Flutter Web端焦点管理的内部冲突问题,和手动调用焦点API无关,主要诱因包括:

  1. Flutter 3.7.3版本的Web端焦点处理存在已知bug,页面导航时焦点状态更新逻辑冲突
  2. ElevatedButton在Web端默认会自动获取焦点,跳转页面时原按钮的焦点未被正确释放,导致焦点树更新异常
  3. ScaffoldMessenger嵌套在HomeScreen内部,可能干扰了全局焦点树的正常层级结构

解决方案

1. 升级Flutter版本

直接升级到3.7.5及以上的稳定版,官方已经修复了该版本批次的Web焦点相关bug,这是最彻底的解决方式。

2. 手动清除焦点再导航

在按钮的onPressed回调中,先主动清除当前焦点,再执行页面跳转:

CustomButton(
  onPressed: () {
    // 清除当前焦点
    FocusManager.instance.primaryFocus?.unfocus();
    Navigator.of(context).pushNamed("/bluetooth-devices");
  },
  // ...其他参数
)

3. 调整ScaffoldMessenger结构

将ScaffoldMessenger移到MaterialApp的子组件层级,而非嵌套在HomeScreen中,确保全局消息处理和焦点树的正确关联:

// 示例结构
MaterialApp(
  home: ScaffoldMessenger(
    child: HomeScreen(),
  ),
);

4. 禁用按钮自动焦点

给ElevatedButton添加autofocus: false属性,避免按钮自动获取焦点引发冲突:

// 在CustomButton的build方法中
return ElevatedButton(
  autofocus: false, // 添加这一行
  onPressed: onPressed,
  style: style,
  child: spinner != null ? const CircularProgressIndicator() : child,
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 18:27:18