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
相关产品推荐
相关产品推荐

