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

Flutter:API返回数据后setState更新下拉项聚焦时不生效

问题排查:下拉选择器更新后需失焦再聚焦才显示新选项

问题描述

获取API返回数据后通过setState更新下拉选择器的选项,组件已完成重建,但下拉菜单的选项并未实时更新,只有失焦后再次聚焦才能看到更新后的内容。

相关代码

自定义异步下拉组件(AppAsyncDropdown)

import 'dart:async';
import 'package:eas/app/core/theme/colors.dart';
import 'package:eas/app/core/theme/theme.dart';
import 'package:eas/app/core/widgets/form_field_input.dart';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';

class AppAsyncDropdown extends StatefulWidget {
  final double width;
  final String hint;
  final bool loading;
  final List<DropdownMenuItem> items;
  final Function(String search) onChanged;
  const AppAsyncDropdown({
    Key? key,
    required this.width,
    required this.hint,
    required this.loading,
    required this.items,
    required this.onChanged,
  }) : super(key: key);

  @override
  State<AppAsyncDropdown> createState() => _AppAsyncDropdownState();
}

class _AppAsyncDropdownState extends State<AppAsyncDropdown> {
  Timer? _timer;

  void debounceSearch(String search) {
    if (_timer != null) {
      _timer!.cancel();
    }
    _timer = Timer(
        const Duration(milliseconds: 400), (() => widget.onChanged(search)));
  }

  @override
  void dispose() {
    if (_timer != null) {
      _timer!.cancel();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print('why.. ${widget.loading} - ${widget.items}');
    final items = [
      DropdownMenuItem(
        enabled: false,
        child: FormFieldInput(
          controller: TextEditingController(),
          width: widget.width,
          hintText: 'search product',
          onChanged: (value) => debounceSearch(value),
        ),
      ),
    ];
    if (widget.loading) {
      items.add(
        DropdownMenuItem(
          enabled: false,
          child: SizedBox(
            width: widget.width,
            child: Center(
                child: Lottie.asset(
              'assets/animation/search_loading.json',
              width: 150,
            )),
          ),
        ),
      );
    }
    if (widget.items.isNotEmpty) {
      items.addAll(widget.items);
    }
    return SizedBox(
      width: widget.width,
      child: DropdownButtonFormField<dynamic>(
        isExpanded: true,
        decoration: InputDecoration(
          hintText: widget.hint,
          hintStyle: AppTheme.of(context).subtitle4,
          border: const UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.black)),
          focusedBorder: const UnderlineInputBorder(
            borderSide: BorderSide(color: AppColors.primary),
          ),
        ),
        items: items,
        onChanged: (dynamic e) {
          print('hii $e');
        },
      ),
    );
  }
}

状态更新页面(TransferStockForm)

import 'dart:async';

import 'package:eas/app/core/layout/desktop/content_desktop.dart';
import 'package:eas/app/core/layout/desktop/header_desktop.dart';
import 'package:eas/app/core/layout/desktop/layout_desktop.dart';
import 'package:eas/app/core/theme/colors.dart';
import 'package:eas/app/core/theme/miscs.dart';
import 'package:eas/app/core/theme/sizedBoxes.dart';
import 'package:eas/app/core/theme/sizes.dart';
import 'package:eas/app/core/theme/theme.dart';
import 'package:eas/app/core/widgets/button.dart';
import 'package:eas/app/core/widgets/dropdown.dart';
import 'package:eas/app/core/widgets/dynamic_list.dart';
import 'package:eas/app/routes/app_pages.dart';
import 'package:flutter/material.dart';

class TransferStockForm extends StatefulWidget {
  const TransferStockForm({Key? key}) : super(key: key);

  @override
  State<TransferStockForm> createState() => _TransferStockFormState();
}

class _TransferStockFormState extends State<TransferStockForm> {
  Timer? _timer;
  List<DropdownMenuItem> _branchWarehouses = [];
  bool _isLoading = false;

  void updateBranchWarehouse(String search) {
    print('searched - $search');
    setState(() {
      _isLoading = true;
      _branchWarehouses = [];
    });
    _timer = Timer(
      const Duration(seconds: 2),
      () {
        setState(() {
          _isLoading = false;
          _branchWarehouses = const [
            DropdownMenuItem(
              value: '1',
              child: Text('one'),
            ),
            DropdownMenuItem(
              value: '2',
              child: Text('two'),
            ),
            DropdownMenuItem(
              value: '3',
              child: Text('three'),
            ),
          ];
        });
      },
    );
  }

  @override
  void dispose() {
    if (_timer != null) {
      _timer!.cancel();
    }

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AppDesktopLayout(
          header: const AppDesktopHeader(
            title: "Transfer Stock",
            pageName: AppRoutes.transferStockForm,
            isFavourable: true,
          ),
          currentNavi: AppRoutes.home,
          content: AppDesktopContent(
            child: Container(
              padding: const EdgeInsets.all(AppSizes.md),
              child: Container(
                padding: const EdgeInsets.all(AppSizes.md),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: const BorderRadius.all(
                    Radius.circular(AppSizes.sm),
                  ),
                  boxShadow: [
                    AppThemeMiscs.shadow2,
                  ],
                ),
                child: LayoutBuilder(builder: (context, constraint) {
                  return Row(
                    children: [
                      SizedBox(
                        width: constraint.maxWidth * 0.35,
                        height: constraint.maxHeight,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              'To',
                              style: AppTheme.of(context).headline3,
                            ),
                            AppSizeBoxes.sm,
                            AppAsyncDropdown(
                              width: constraint.maxWidth * 0.35,
                              hint: 'select branch or warehouse to transfer to',
                              loading: _isLoading,
                              items: _branchWarehouses,
                              onChanged: (String search) =>
                                  updateBranchWarehouse(search),
                            ),
                            AppSizeBoxes.md,
                            Button(onPressed: () {}, text: 'Transfer'),
                          ],
                        ),
                      ),
                      const Expanded(
                          child: DynamicList(
                        type: 'stock',
                      )),
                    ],
                  );
                }),
              ),
            ),
          )),
    );
  }
}

问题根源

Flutter的DropdownButtonFormField下拉菜单是通过Overlay实现的独立弹窗,当下拉菜单处于展开状态时,父组件的重建不会同步更新Overlay中的内容。即使items已经更新,已展开的菜单仍然会显示旧数据,直到菜单关闭后重新打开。

解决方案

通过监听组件更新,在下拉选项变化时自动关闭菜单,让用户重新打开即可看到最新内容。修改AppAsyncDropdown的状态类如下:

class _AppAsyncDropdownState extends State<AppAsyncDropdown> {
  Timer? _timer;
  // 添加GlobalKey用于控制DropdownButtonFormField状态
  final _dropdownKey = GlobalKey<DropdownButtonFormFieldState>();

  void debounceSearch(String search) {
    if (_timer != null) {
      _timer!.cancel();
    }
    _timer = Timer(
        const Duration(milliseconds: 400), (() => widget.onChanged(search)));
  }

  // 重写didUpdateWidget,监听items变化
  @override
  void didUpdateWidget(covariant AppAsyncDropdown oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 当选项列表发生变化时,关闭下拉菜单
    if (widget.items != oldWidget.items) {
      _dropdownKey.currentState?.close();
    }
  }

  @override
  void dispose() {
    if (_timer != null) {
      _timer!.cancel();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print('why.. ${widget.loading} - ${widget.items}');
    final items = [
      DropdownMenuItem(
        enabled: false,
        child: FormFieldInput(
          controller: TextEditingController(),
          width: widget.width,
          hintText: 'search product',
          onChanged: (value) => debounceSearch(value),
        ),
      ),
    ];
    if (widget.loading) {
      items.add(
        DropdownMenuItem(
          enabled: false,
          child: SizedBox(
            width: widget.width,
            child: Center(
                child: Lottie.asset(
              'assets/animation/search_loading.json',
              width: 150,
            )),
          ),
        ),
      );
    }
    if (widget.items.isNotEmpty) {
      items.addAll(widget.items);
    }
    return SizedBox(
      width: widget.width,
      child: DropdownButtonFormField<dynamic>(
        // 绑定GlobalKey
        key: _dropdownKey,
        isExpanded: true,
        decoration: InputDecoration(
          hintText: widget.hint,
          hintStyle: AppTheme.of(context).subtitle4,
          border: const UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.black)),
          focusedBorder: const UnderlineInputBorder(
            borderSide: BorderSide(color: AppColors.primary),
          ),
        ),
        items: items,
        onChanged: (dynamic e) {
          print('hii $e');
        },
      ),
    );
  }
}

额外优化建议

  • 搜索框的TextEditingController每次build都重新创建,可能导致输入状态丢失,建议将其提升为State类成员变量,并在dispose时释放。
  • 可以考虑使用ValueNotifier结合AnimatedBuilder来更高效地更新下拉菜单内容,避免不必要的组件重建。

内容的提问来源于stack exchange,提问作者Tin Ko Ko Win

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 23:45:35