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

Flutter如何创建平滑展开的下拉列表?

实现可平滑展开的下拉列表(Flutter)

嘿,作为Flutter新手,你想要的这种可展开收起的列表,用Flutter自带的ExpansionTile组件就能轻松实现——它内置了平滑的展开/收起动画,完全符合你的需求!我会结合你的现有代码,给你改造出可运行的实现方案。

步骤1:定义数据模型

首先,我们需要一个数据模型来对应API返回的分类和子测试数据,这样处理起来更清晰:

// 定义分类模型,包含子测试列表
class PracticeTestCategory {
  final String title;
  final int submittedCount;
  final List<String> children; // 子测试名称列表,可根据实际API结构调整

  PracticeTestCategory({
    required this.title,
    required this.submittedCount,
    required this.children,
  });
}

步骤2:改造你的状态组件

接下来,修改你的_PracticetestComp类,集成ExpansionTile并处理API数据:

import 'package:flutter/material.dart';
import 'package:lpa_exam/src/api/api_manager.dart';

class Practicetest extends StatefulWidget {
  final examId;
  Practicetest({Key? key, this.examId}) : super(key: key);

  @override
  _PracticetestComp createState() => _PracticetestComp();
}

class _PracticetestComp extends State<Practicetest> {
  List<PracticeTestCategory>? _practiceTestCategories;
  bool _isLoading = true; // 新增加载状态标识

  @override
  void initState() {
    super.initState();
    _fetchPracticeTests();
  }

  // 重构API请求方法,增加错误处理和加载状态
  Future<void> _fetchPracticeTests() async {
    try {
      final result = await APIManager.getpracticetestlist(widget.examId);
      // 这里根据实际API返回结构,将result转换为PracticeTestCategory列表
      // 示例模拟转换(你需要替换成真实的解析逻辑)
      setState(() {
        _practiceTestCategories = result.map((item) => PracticeTestCategory(
          title: item['title'],
          submittedCount: item['submitted_count'],
          children: List<String>.from(item['children']),
        )).toList();
      });
    } catch (e) {
      // 这里可以添加错误提示,比如SnackBar
      print('加载数据失败: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xffF8FDF7),
      appBar: AppBar(
        backgroundColor: const Color(0xffF8FDF7),
        brightness: Brightness.light,
        elevation: 0.0,
        leading: Container(
          margin: const EdgeInsets.only(left: 17),
          child: RawMaterialButton(
            onPressed: () {
              Navigator.pushNamed(context, '/');
            },
            child: const Icon(
              Icons.keyboard_backspace,
              color: Colors.redAccent,
              size: 25.0,
            ),
            shape: const CircleBorder(),
            elevation: 4.0,
            fillColor: Colors.white,
            padding: const EdgeInsets.all(5.0),
          ),
        ),
      ),
      body: Container(
        margin: const EdgeInsets.only(top: 40, left: 30, right: 30),
        child: _isLoading 
          ? const Center(child: CircularProgressIndicator()) // 加载中显示指示器
          : ListView(
              shrinkWrap: true,
              children: [
                // 标题部分
                const ListTile(
                  dense: true,
                  contentPadding: EdgeInsets.all(0),
                  title: Text(
                    'Quick set of',
                    style: TextStyle(
                      fontWeight: FontWeight.w500,
                      fontSize: 18,
                    ),
                  ),
                  subtitle: Text(
                    'Practice Tests',
                    style: TextStyle(
                      fontWeight: FontWeight.w700,
                      fontSize: 22,
                      color: Colors.black),
                  ),
                ),
                const SizedBox(height: 30),
                // 动态生成可展开列表
                if (_practiceTestCategories != null)
                  ..._practiceTestCategories!.map((category) {
                    return Column(
                      children: [
                        ExpansionTile(
                          contentPadding: EdgeInsets.zero,
                          leading: Text(
                            category.title,
                            style: const TextStyle(
                              color: Colors.black,
                              fontWeight: FontWeight.w500,
                            ),
                          ),
                          trailing: Text(
                            '${category.submittedCount} Submitted',
                            style: const TextStyle(color: Colors.grey),
                          ),
                          // 子列表内容,这里用简单的ListTile示例,你可以自定义样式
                          children: category.children.map((child) {
                            return Padding(
                              padding: const EdgeInsets.only(left: 20),
                              child: ListTile(
                                title: Text(child),
                                onTap: () {
                                  // 点击子项的逻辑,比如跳转到测试页面
                                  print('点击了子项: $child');
                                },
                              ),
                            );
                          }).toList(),
                        ),
                        // 每个分类之间的分隔线
                        const Divider(color: Colors.grey, thickness: 1),
                      ],
                    );
                  }).toList(),
              ],
            ),
      ),
    );
  }
}

关键说明

  1. ExpansionTile的核心作用:它自动处理展开/收起的平滑动画,你只需要指定leading(左侧标题)、trailing(右侧状态文本)和children(展开后的子列表)即可。
  2. 加载状态处理:新增了_isLoading变量,在API请求完成前显示加载指示器,提升用户体验。
  3. 数据解析:你需要根据API返回的真实数据结构,调整_fetchPracticeTests里的模型转换逻辑,确保能正确把API数据转成PracticeTestCategory列表。
  4. 子项自定义:子列表的样式可以完全自定义,比如替换成卡片、自定义Widget等,满足你的UI需求。

如果需要更定制化的展开动画或样式,也可以自己用AnimatedContainer结合GestureDetector实现,但对于新手来说,ExpansionTile是最快捷的方案!

内容的提问来源于stack exchange,提问作者Gaurav Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 19:57:41