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
相关产品推荐
相关产品推荐

