Flutter ListTile中临时显示CircularProgressIndicator的实现方案
解决方案:ListTile trailing 切换加载状态实现
要实现开关切换时临时显示加载指示器,核心是为每个技能单独维护加载状态,避免单个状态变量导致所有列表项同时触发加载。以下是具体修改方案:
关键改动步骤
- 添加加载状态管理
在_EditStudentSkillsState类中添加一个Map,用于记录每个技能的加载状态(通过skillNumber作为唯一标识):
Map<String, bool> _loadingStates = {};
- 初始化加载状态
在FutureBuilder获取到技能数据后,初始化所有技能的加载状态为false:
if (snapshot.hasData) { List<SkillModel> skillList = snapshot.data as List<SkillModel>; // 初始化所有技能的加载状态 if (_loadingStates.isEmpty) { for (var skill in skillList) { _loadingStates[skill.number] = false; } } // ... 后续ListView构建代码 }
- 修改API请求方法
将updateSkill改为异步返回方法,在请求开始时标记加载状态,请求结束(无论成功/失败)时取消加载,同时处理响应失败时的状态回滚:
Future<void> updateSkill(String skillNumber, bool newValue, SkillModel skill) async { // 标记当前技能为加载中 setState(() { _loadingStates[skillNumber] = true; }); var updateUrl = Uri.parse( "${AppConfig.of(context).apiURL}/api/driving-skill/update/${widget.student.uid}"); var headers = { 'Content-type': 'application/json', 'Accept': 'application/json', }; var requestBody = json.encode({"skillNumber": skillNumber, "value": newValue}); try { var response = await http.patch(updateUrl, headers: headers, body: requestBody, encoding: Encoding.getByName("utf8")); // 响应非200时回滚开关状态 if (response.statusCode != 200) { setState(() { skill.completed = !newValue; }); // 可在此处添加错误提示 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('技能更新失败,请重试')) ); } } catch (e) { // 网络异常时回滚状态 setState(() { skill.completed = !newValue; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('网络错误,请检查连接')) ); } finally { // 无论请求结果如何,结束加载状态 setState(() { _loadingStates[skillNumber] = false; }); } }
- 动态渲染Trailing区域
根据当前技能的加载状态,切换显示CircularProgressIndicator或CupertinoSwitch:
trailing: _loadingStates[skill.number] == true ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), // 缩小指示器适配布局 ) : CupertinoSwitch( value: skill.completed, onChanged: (value) async { setState(() { skill.completed = value; }); // 等待请求完成 await updateSkill(skill.number, value, skill); }, ),
修改后的完整代码
import 'dart:convert'; import 'dart:io'; import 'package:excelerate/model/user_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../../app_config.dart'; import '../../model/skill_model.dart'; import '../../providers/app_colors.dart'; import '../../providers/skill_order.dart'; import '../../providers/user_secure_storage.dart'; import '../../providers/utils.dart'; import '../../widgets/excelerate_bottom_bar.dart'; class EditStudentSkills extends StatefulWidget { final UserModel student; const EditStudentSkills({Key? key, required this.student}) : super(key: key); @override State<EditStudentSkills> createState() => _EditStudentSkillsState(); } class _EditStudentSkillsState extends State<EditStudentSkills> { late Future<List<SkillModel>> skills; // 维护每个技能的加载状态 Map<String, bool> _loadingStates = {}; @override void initState() { super.initState(); skills = fetchSkills(); } @override EditStudentSkills get widget => super.widget; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, backgroundColor: ColorConsts.excelerateBlue, title: Text( 'Rijvaardigheden ${widget.student.firstName}', textAlign: TextAlign.start, ), ), body: SafeArea( child: FutureBuilder<List<SkillModel>>( future: skills, builder: (context, snapshot) { if (snapshot.hasData) { List<SkillModel> skillList = snapshot.data as List<SkillModel>; // 初始化加载状态 if (_loadingStates.isEmpty) { for (var skill in skillList) { _loadingStates[skill.number] = false; } } return ListView.separated( itemCount: skillList.length, scrollDirection: Axis.vertical, itemBuilder: (BuildContext context, int index) { var skill = skillList[index]; return ListTile( contentPadding: const EdgeInsets.only(left: 20, right: 20), leading: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('${skill.number}.', textAlign: TextAlign.center), ], ), title: Text(skill.name), trailing: _loadingStates[skill.number] == true ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : CupertinoSwitch( value: skill.completed, onChanged: (value) async { setState(() { skill.completed = value; }); await updateSkill(skill.number, value, skill); }, ), ); }, separatorBuilder: (BuildContext context, int index) { return const Divider(); }, ); } else if (snapshot.hasError) { return Text(snapshot.error.toString()); } // 默认显示全局加载指示器 return const Center(child: CircularProgressIndicator()); }, ), ), bottomNavigationBar: ExcelerateBottomBar( currentIndex: 1, onTap: navigate, ), ); } void navigate(int index) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => Utils.getInstructorScreen(index))); } Future<List<SkillModel>> fetchSkills() async { var bearer = await UserSecureStorage.getBearerToken(); var headers = { 'Accept': 'application/json', HttpHeaders.authorizationHeader: bearer.toString() }; var infoUrl = Uri.parse( "${AppConfig.of(context).apiURL}/api/skill/all-with-status/${widget.student.uid}"); var response = await http.get(infoUrl, headers: headers); var skillMap = await json.decode(response.body); var skillList = <SkillModel>[]; for (int i = 0; i < skillMap.keys.length; i++) { var skillName = SkillOrder.order[i]; skillList.add(SkillModel.fromMap(skillMap[skillName])); } return skillList; } Future<void> updateSkill(String skillNumber, bool newValue, SkillModel skill) async { setState(() { _loadingStates[skillNumber] = true; }); var updateUrl = Uri.parse( "${AppConfig.of(context).apiURL}/api/driving-skill/update/${widget.student.uid}"); var headers = { 'Content-type': 'application/json', 'Accept': 'application/json', }; var requestBody = json.encode({"skillNumber": skillNumber, "value": newValue}); try { var response = await http.patch(updateUrl, headers: headers, body: requestBody, encoding: Encoding.getByName("utf8")); if (response.statusCode != 200) { setState(() { skill.completed = !newValue; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('技能更新失败,请重试')) ); } } catch (e) { setState(() { skill.completed = !newValue; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('网络错误,请检查连接')) ); } finally { setState(() { _loadingStates[skillNumber] = false; }); } } }
说明
- 每个技能独立维护加载状态,不会互相干扰
- 请求失败/网络异常时自动回滚开关状态,保证界面数据一致性
- 加载指示器使用缩小尺寸的
CircularProgressIndicator,适配ListTile的布局空间
内容的提问来源于stack exchange,提问作者Milan Dol
相关产品推荐
相关产品推荐

