Flutter下拉菜单即时切换+加载数据时显示进度指示器方案
问题描述
我正在构建一个展示与当前选中分类匹配条目的UI,核心功能已实现,但希望在获取新选中分类的条目时显示Progress Indicator。当前UI需等待数据获取完成后才切换选中项,显得不够流畅。请问如何修改代码,让下拉菜单的选中项即时切换,加载匹配条目时显示进度指示器,数据就绪后再显示条目字段?
解决方案
调整状态管理逻辑,分离下拉选中状态与数据加载状态,实现即时切换+加载提示:
- 添加加载状态标记:新增
isLoading布尔变量,用于控制进度指示器的显示与隐藏 - 即时更新下拉选中值:在下拉选项变更时立即更新
dropdownValue并刷新UI,再触发数据加载 - 异步加载数据并更新状态:加载数据前标记为加载中,加载完成后更新设备列表并取消加载标记
- 根据状态渲染对应UI:加载中显示进度指示器,加载完成后根据设备列表内容显示条目或空提示
修改后的完整代码
import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); runApp(const SignUpApp()); } class SignUpApp extends StatelessWidget { const SignUpApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( routes: { '/': (context) => BrowseScreen(), '/welcome': (context) => const WelcomeScreen(), '/browse': (context) => BrowseScreen(), }, ); } } class BrowseScreen extends StatefulWidget { BrowseScreen({super.key}); @override State<BrowseScreen> createState() => _BrowseScreenState(); } class _BrowseScreenState extends State<BrowseScreen> { late final Future<List<EquipmentCategory>> categories; late List<Equipment> equipment = []; String dropdownValue = ''; bool isLoading = false; // 新增加载状态变量 //For local testing without Firebase final List<EquipmentCategory> equipmentCategories = [ EquipmentCategory('1iyQmcbWIV6gNMk5PX9E', 'Lawn & Garden - motorised'), EquipmentCategory('ofYNwqKtvbctjiwETKOZ', 'Trailers'), EquipmentCategory('r5FSAIyIrinkr8m1TXTj', 'Lawn & Garden - manual'), ]; final List<Equipment> trailerEquipmentList = [Equipment(id: 'd9A8qfHT8lKIzhUk55K5', name: 'Two-wheel trailer 900mm', categories: ['ofYNwqKtvbctjiwETKOZ'], photos: ["https://media.prod.bunnings.com.au/api/public/content/c65c035a7c7b4a7282c3b5f3def5ec76?v=94a4edf7&t=w500dpr1"]) ]; late final Map<String, List<Equipment>> equipmentByCategory = { 'ofYNwqKtvbctjiwETKOZ': trailerEquipmentList, '1iyQmcbWIV6gNMk5PX9E': [], 'r5FSAIyIrinkr8m1TXTj': [] }; @override void initState() { super.initState(); categories = _fetchEquipmentCategories(); dropdownValue = '1iyQmcbWIV6gNMk5PX9E'; // 初始化加载默认分类数据 _fetchEquipment(dropdownValue).then((fetchedEquipment) { setState(() { equipment = fetchedEquipment ?? []; }); }); } @override Widget build(BuildContext context) { return Material( child: FutureBuilder( future: categories, builder: (BuildContext context, AsyncSnapshot snapshot) { if (!snapshot.hasData) { // 分类数据加载中 return Center( child: CircularProgressIndicator( backgroundColor: Colors.red, valueColor: new AlwaysStoppedAnimation<Color>(Colors.blue)), ); } else { return Container( color: CupertinoColors.white, padding: const EdgeInsets.only(top: 20), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Column(children: [ DropdownButton<String>( icon: const Icon(Icons.arrow_downward), elevation: 16, style: const TextStyle(color: Colors.deepPurple), underline: Container( height: 2, color: Colors.deepPurpleAccent, ), onChanged: (String? value) { if (value == null) return; // 立即更新选中值并触发UI刷新 setState(() { dropdownValue = value; isLoading = true; }); // 异步加载对应分类数据 _fetchEquipment(value).then((fetchedEquipment) { setState(() { equipment = fetchedEquipment ?? []; isLoading = false; }); }); }, items: snapshot.data .map<DropdownMenuItem<String>>( (EquipmentCategory value) { return DropdownMenuItem<String>( value: value.id, child: Text(value.name), ); }).toList(), value: dropdownValue, ), const SizedBox(height: 20), // 根据状态显示不同内容 if (isLoading) const Center(child: CircularProgressIndicator()) else if (equipment.isNotEmpty) Column( children: equipment.map((item) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text(item.name, style: const TextStyle(color: Colors.black, fontSize: 16)), )).toList(), ) else const Text('EMPTY', style: TextStyle(fontSize: 16)) ]) ])); } })); } Future<List<EquipmentCategory>> _fetchEquipmentCategories() async { return Future.delayed( const Duration(seconds: 1), () => equipmentCategories, ); } // 修改为接收分类ID参数 Future<List<Equipment>?> _fetchEquipment(String categoryId) async { await Future.delayed(const Duration(seconds: 1)); return equipmentByCategory[categoryId]; } } class EquipmentCategory { String id; String name; EquipmentCategory(this.id, this.name); } class Equipment { late String id; late String name; late List<String> categories; late List<String> photos; Equipment({required id, required name, required categories, required photos}); factory Equipment.fromJson(Map<String, dynamic> json) { return Equipment( id: json['id'], name: json['name'], categories: json['categories'], photos: json['photos']); } } class _WelcomeFormState extends State<WelcomeScreen> { @override Widget build(BuildContext context) { return Container( color: CupertinoColors.white, padding: const EdgeInsets.only(top: 20), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ MyButton( text: 'Browse', icon: CupertinoIcons.search, callback: _showAvailableEquipmentScreen, ), MyButton( text: 'Return', icon: Icons.recycling, callback: () {}, ), MyButton( text: 'Login', icon: Icons.login, callback: () { showCupertinoDialog( context: context, builder: (context) => CupertinoAlertDialog( title: const Text('Hello, World!'), actions: [ CupertinoDialogAction( child: const Text('Close'), onPressed: () { Navigator.pop(context); }, ), ], ), ); }, ), ], ), ); } void _showAvailableEquipmentScreen() { Navigator.of(context).pushNamed('/browse'); } } class WelcomeScreen extends StatefulWidget { const WelcomeScreen({super.key}); @override State<WelcomeScreen> createState() => _WelcomeFormState(); } class MyButton extends StatelessWidget { const MyButton({ super.key, required this.text, required this.icon, required this.callback, }); final String text; final IconData icon; final VoidCallback callback; final double buttonSize = 90; @override Widget build(BuildContext context) { return CupertinoButton( padding: EdgeInsets.zero, minSize: 0, onPressed: callback, child: Container( decoration: BoxDecoration( color: const Color(0xFFEAEAEA), borderRadius: BorderRadius.circular(15), ), padding: const EdgeInsets.symmetric( vertical: 20, horizontal: 30, ), child: Column( children: [ Stack( children: [ Container( width: buttonSize, height: buttonSize, decoration: BoxDecoration( color: CupertinoColors.black, borderRadius: BorderRadius.circular(17), ), ), Container( width: buttonSize, height: buttonSize, decoration: BoxDecoration( color: CupertinoColors.white, borderRadius: BorderRadius.circular(20), ), child: Icon( icon, size: 60, color: CupertinoColors.black, ), ), ], ), const SizedBox(height: 10), Text( text, style: const TextStyle( fontWeight: FontWeight.w600, color: CupertinoColors.black, ), ) ], ), ), ); } }
内容的提问来源于stack exchange,提问作者Konstantin
相关产品推荐
相关产品推荐

