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

Flutter不规则层级多维数组数据建模与Expansion Tile优化

Flutter 不规则层级课程数据解析与ExpansionTile性能优化方案

问题核心

  • 给定JSON存在不规则层级:UNIT-I、UNIT-II是「单元→主题→课程」三级结构,UNIT-III直接为「单元=课程」二级结构
  • 原始数据类未适配动态层级,解析逻辑混乱,无法正确映射所有数据
  • 需要兼顾ExpansionTile渲染性能,避免层级深、数据多导致的卡顿

一、数据类重构:递归节点模型

针对不规则层级,设计通用递归节点结构,统一处理文件夹型节点(含子节点)和叶子型节点(含课程详情):

import 'dart:convert';

// API响应顶层模型
class ApiResponse {
  final bool status;
  final String message;
  final Map<String, dynamic> data;

  ApiResponse({
    required this.status,
    required this.message,
    required this.data,
  });

  factory ApiResponse.fromMap(Map<String, dynamic> json) => ApiResponse(
        status: json["status"],
        message: json["message"],
        data: json["data"],
      );

  // 将顶层数据转换为根节点列表
  List<TreeNode> getRootNodes() {
    return data.entries.map((entry) {
      return TreeNode.fromMap(entry.key, entry.value);
    }).toList();
  }
}

// 通用递归节点:既可以是文件夹(含子节点),也可以是课程(含详情)
class TreeNode {
  final String title;
  final bool isLesson;
  final List<LessonDetail>? lessonDetails;
  final List<TreeNode>? children;

  TreeNode({
    required this.title,
    required this.isLesson,
    this.lessonDetails,
    this.children,
  });

  factory TreeNode.fromMap(String nodeTitle, Map<String, dynamic> map) {
    // 判断当前节点是否为课程节点
    final isLesson = map.containsKey("is_lesson") && map["is_lesson"] == true;

    if (isLesson) {
      // 叶子节点:解析课程详情
      final details = map["lesson_detail"] != null
          ? List<LessonDetail>.from(
              map["lesson_detail"].map((x) => LessonDetail.fromMap(x)))
          : null;
      return TreeNode(
        title: nodeTitle,
        isLesson: true,
        lessonDetails: details,
        children: null,
      );
    } else {
      // 文件夹节点:递归解析子节点
      final children = map.entries.map((entry) {
        return TreeNode.fromMap(entry.key, entry.value);
      }).toList();
      return TreeNode(
        title: nodeTitle,
        isLesson: false,
        lessonDetails: null,
        children: children,
      );
    }
  }
}

// 课程详情模型
class LessonDetail {
  final String title;
  final String lessonId;
  final String courseId;
  final String sectionId;
  final String duration;
  final String isLessonFree;
  final String videoUrl;
  final String videoType;
  final String attachmentType;
  final dynamic attachment;

  LessonDetail({
    required this.title,
    required this.lessonId,
    required this.courseId,
    required this.sectionId,
    required this.duration,
    required this.isLessonFree,
    required this.videoUrl,
    required this.videoType,
    required this.attachmentType,
    required this.attachment,
  });

  factory LessonDetail.fromMap(Map<String, dynamic> json) => LessonDetail(
        title: json["title"],
        lessonId: json["lesson_id"],
        courseId: json["course_id"],
        sectionId: json["section_id"],
        duration: json["duration"],
        isLessonFree: json["is_lesson_free"],
        videoUrl: json["video_url"],
        videoType: json["video_type"],
        attachmentType: json["attachment_type"],
        attachment: json["attachment"],
      );
}

// 解析工具方法
ApiResponse apiResponseFromJson(String str) => ApiResponse.fromMap(json.decode(str));

解析逻辑说明

  • 用TreeNode统一所有节点类型,通过isLesson区分课程/文件夹
  • 递归解析:节点含is_lesson且为true时作为课程叶子节点,否则作为文件夹继续解析子节点
  • 顶层ApiResponse的getRootNodes()直接返回根节点列表,无需手动处理层级差异

二、ExpansionTile性能优化方案

1. 懒加载子项

使用ListView.builder替代直接遍历,只渲染可见区域组件:

class TreeNodeExpansionTile extends StatelessWidget {
  final TreeNode node;

  const TreeNodeExpansionTile({super.key, required this.node});

  @override
  Widget build(BuildContext context) {
    if (node.isLesson) {
      // 课程节点:直接展示详情
      return ListTile(
        title: Text(node.title),
        subtitle: node.lessonDetails?.isNotEmpty == true
            ? Text(node.lessonDetails!.first.duration)
            : null,
      );
    } else {
      // 文件夹节点:包裹懒加载子项
      return ExpansionTile(
        title: Text(node.title),
        children: [
          ListView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            itemCount: node.children?.length ?? 0,
            itemBuilder: (context, index) {
              return TreeNodeExpansionTile(node: node.children![index]);
            },
          ),
        ],
      );
    }
  }
}

2. 保留展开状态

用AutomaticKeepAliveClientMixin维持ExpansionTile的展开/折叠状态:

class TreeNodeExpansionTile extends StatefulWidget {
  final TreeNode node;

  const TreeNodeExpansionTile({super.key, required this.node});

  @override
  State<TreeNodeExpansionTile> createState() => _TreeNodeExpansionTileState();
}

class _TreeNodeExpansionTileState extends State<TreeNodeExpansionTile> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    // 原有build逻辑...
  }
}

3. 预处理缓存

提前缓存节点展开状态,避免重复计算:

// 全局/组件内缓存展开状态
final Map<String, bool> expansionStates = {};

// 构建ExpansionTile时使用
ExpansionTile(
  title: Text(node.title),
  initiallyExpanded: expansionStates[node.title] ?? false,
  onExpansionChanged: (expanded) {
    setState(() {
      expansionStates[node.title] = expanded;
    });
  },
  // ...子项逻辑
)

4. 减少不必要重建

  • 用const修饰静态子组件(如无状态ListTile)
  • 避免在build中执行解析、排序等耗时操作,提前在预处理阶段完成
  • 数据量大时用ListView.separated减少布局嵌套

三、使用示例

// 解析JSON
final response = apiResponseFromJson(jsonString);
final rootNodes = response.getRootNodes();

// 构建UI
ListView.builder(
  itemCount: rootNodes.length,
  itemBuilder: (context, index) {
    return TreeNodeExpansionTile(node: rootNodes[index]);
  },
)

内容的提问来源于stack exchange,提问作者Abim Joshi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 13:30:41