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

Flutter ListTile中临时显示CircularProgressIndicator的实现方案

解决方案:ListTile trailing 切换加载状态实现

要实现开关切换时临时显示加载指示器,核心是为每个技能单独维护加载状态,避免单个状态变量导致所有列表项同时触发加载。以下是具体修改方案:

关键改动步骤

  1. 添加加载状态管理
    在_EditStudentSkillsState类中添加一个Map,用于记录每个技能的加载状态(通过skillNumber作为唯一标识):
Map<String, bool> _loadingStates = {};
  1. 初始化加载状态
    在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构建代码
}
  1. 修改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;
    });
  }
}
  1. 动态渲染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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 11:24:24