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

Flutter桌面端添加ChangeNotifierProvider后DropdownMenuEntry鼠标点击失效

问题

在Windows平台的Flutter桌面应用中,给main.dart添加ChangeNotifierProvider用于检测本地化变更后,未使用该Provider的视图里的DropdownMenuEntry无法响应鼠标点击,回滚到无该Provider的版本后问题消失。

应用内其他按钮点击正常,鼠标悬停DropdownMenuEntry时会显示手型指针(聚焦状态正常)。DropdownMenu支持键盘操作:通过键盘选中条目回车可正常触发,但禁用键盘输入后鼠标点击问题仍未解决。

简化后的示例代码

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:hub/app/app.bottomsheets.dart';
import 'package:hub/app/app.dialogs.dart';
import 'package:hub/app/app.locator.dart';
import 'package:hub/app/app.router.dart';
import 'package:hub/services/localization_service.dart';
import 'package:hub/ui/common/app_name.dart';
import 'package:hub/ui/common/app_notifier.dart';
import 'package:hub/ui/common/app_window.dart';
import 'package:hub/ui/common/app_themes.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:stacked_themes/stacked_themes.dart';
import 'package:url_strategy/url_strategy.dart';

Future<void> main(List<String> args) async {
  print(args.toString());

  final appName = getAppName();

  WidgetsFlutterBinding.ensureInitialized();
  await ThemeManager.initialise();
  setPathUrlStrategy();
  await setupLocator(stackedRouter: stackedRouter);
  setupDialogUi();
  setupBottomSheetUi();
  await setupNotifications(appName);

  final lightTheme = await buildLightTheme();
  final darkTheme = await buildDarkTheme();

  runApp(MainApp(
    appName: appName,
    lightTheme: lightTheme,
    darkTheme: darkTheme,
  ));

  setupAppWindow(appName);
}

class MainApp extends StatelessWidget {
  final String appName;
  final ThemeData lightTheme;
  final ThemeData darkTheme;

  const MainApp({
    required this.appName,
    required this.lightTheme,
    required this.darkTheme,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return ThemeBuilder(
      defaultThemeMode: ThemeMode.dark,
      lightTheme: lightTheme,
      darkTheme: darkTheme,
      // statusBarColorBuilder: (theme) => theme?.colorScheme.secondary,
      builder: (context, regularTheme, darkTheme, themeMode) => ResponsiveApp(
        preferDesktop: true,
        builder: (context) => ChangeNotifierProvider(
          create: (context) => LocalizationService(),
          child: Consumer<LocalizationService>(
            builder: (context, localization, child) {
              localization.getSavedLocale(appName);
              return MaterialApp.router(
                theme: regularTheme,
                darkTheme: darkTheme,
                themeMode: themeMode,
                debugShowCheckedModeBanner: false,
                localizationsDelegates: AppLocalizations.localizationsDelegates,
                supportedLocales: AppLocalizations.supportedLocales,
                routerDelegate: stackedRouter.delegate(),
                routeInformationParser: stackedRouter.defaultRouteParser(),
                locale: localization.locale,
              );
            },
          ),
        ),
      ),
    );
  }
}

含DropdownMenu的视图代码

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';

import 'export_import_collection_dialog_model.dart';

class ExportImportCollectionDialog extends StackedView<ExportImportCollectionDialogModel> {
  final DialogRequest request;
  final Function(DialogResponse) completer;

  const ExportImportCollectionDialog({
    required this.request,
    required this.completer,
    super.key,
  });

  @override
  Widget builder(BuildContext context, ExportImportCollectionDialogModel viewModel, Widget? child) {
    final l10n = AppLocalizations.of(context)!;
    final theme = Theme.of(context);

    final myVector = ["one", "two", "three"];

    return Dialog(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
      insetPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
      child: Container(
        width: 500,
        padding: const EdgeInsets.symmetric(vertical: 20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            ListTile(
              leading: Text(
                l10n.exportAndImport,
                style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.w500,
                ),
              ),
              trailing: IconButton(
                onPressed: () => completer(DialogResponse(confirmed: true)),
                icon: const Icon(Icons.close),
              ),
            ),
            const SizedBox(height: 16),
            Column(
              children: [
                DropdownMenu(
                  label: Text(l10n.selectCollection),
                  dropdownMenuEntries: myVector
                      .map(
                        (value) => DropdownMenuEntry(
                          value: value,
                          label: value,
                        ),
                      )
                      .toList(),
                  menuHeight: 200.0,
                  width: 400.0,
                  enableFilter: true,
                  enableSearch: true,
                  onSelected: (value) => {
                    print(value),
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  @override
  ExportImportCollectionDialogModel viewModelBuilder(BuildContext context) => ExportImportCollectionDialogModel();
}
可能的解决方法

1. 移除Consumer builder中的副作用操作

你现在在Consumer的builder方法里直接调用localization.getSavedLocale(appName),这会导致每次LocalizationService更新时都执行该方法,引发不必要的Widget重建,干扰DropdownMenu的状态稳定性。

把初始化操作移到Provider创建时完成:

// 修改MainApp中的ChangeNotifierProvider部分
ChangeNotifierProvider(
  create: (context) {
    final localization = LocalizationService();
    localization.getSavedLocale(appName); // 在这里初始化,而非builder内
    return localization;
  },
  child: Consumer<LocalizationService>(
    builder: (context, localization, child) {
      return MaterialApp.router(
        // ... 原有配置
        locale: localization.locale,
      );
    },
  ),
)

2. 避免重建整个MaterialApp

当前MaterialApp被包裹在Consumer内部,每次本地化变更都会重建整个MaterialApp,这会导致子Widget上下文异常,尤其是桌面平台的弹出层组件。

可以将Consumer移到MaterialApp的builder参数中,只重建需要本地化的部分:

// 修改MainApp的build方法
@override
Widget build(BuildContext context) {
  return ThemeBuilder(
    defaultThemeMode: ThemeMode.dark,
    lightTheme: lightTheme,
    darkTheme: darkTheme,
    builder: (context, regularTheme, darkTheme, themeMode) => ResponsiveApp(
      preferDesktop: true,
      builder: (context) => ChangeNotifierProvider(
        create: (context) {
          final localization = LocalizationService();
          localization.getSavedLocale(appName);
          return localization;
        },
        child: MaterialApp.router(
          theme: regularTheme,
          darkTheme: darkTheme,
          themeMode: themeMode,
          debugShowCheckedModeBanner: false,
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          routerDelegate: stackedRouter.delegate(),
          routeInformationParser: stackedRouter.defaultRouteParser(),
          builder: (context, child) {
            return Consumer<LocalizationService>(
              builder: (context, localization, child) {
                return Localizations.override(
                  context: context,
                  locale: localization.locale,
                  child: child!,
                );
              },
            );
          },
        ),
      ),
    ),
  );
}

3. 优化LocalizationService的通知逻辑

确保LocalizationService只有在locale真正变化时才调用notifyListeners(),避免不必要的通知触发Widget重建:

// LocalizationService中的示例实现
class LocalizationService extends ChangeNotifier {
  Locale? _locale;
  Locale? get locale => _locale;

  void getSavedLocale(String appName) {
    // 从存储读取保存的locale
    final savedLocale = // 你的读取逻辑
    if (savedLocale != _locale) {
      _locale = savedLocale;
      notifyListeners(); // 仅当locale变化时通知
    }
  }
}

4. 手动管理DropdownMenu的焦点(Windows平台专属)

桌面平台的DropdownMenu偶尔会因为焦点层级问题导致点击无响应,可以手动添加焦点节点并管理:

// 修改含DropdownMenu的视图代码
class ExportImportCollectionDialog extends StackedView<ExportImportCollectionDialogModel> {
  final DialogRequest request;
  final Function(DialogResponse) completer;
  final FocusNode _dropdownFocusNode = FocusNode(); // 添加焦点节点

  const ExportImportCollectionDialog({
    required this.request,
    required this.completer,
    super.key,
  });

  @override
  void initState() {
    super.initState();
    // 对话框初始化后请求焦点
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _dropdownFocusNode.requestFocus();
    });
  }

  @override
  void dispose() {
    _dropdownFocusNode.dispose(); // 销毁焦点节点
    super.dispose();
  }

  @override
  Widget builder(BuildContext context, ExportImportCollectionDialogModel viewModel, Widget? child) {
    // ... 原有代码
    return Dialog(
      // ... 原有代码
      child: Container(
        // ... 原有代码
        child: Column(
          // ... 原有代码
          children: [
            // ... 原有代码
            DropdownMenu(
              focusNode: _dropdownFocusNode, // 绑定焦点节点
              // ... 原有配置
            ),
          ],
        ),
      ),
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 16:59:53