Flutter中如何在ListView内部嵌套使用ListView
Flutter 双索引独立模型下嵌套ListView实现方案
两层ListView分别绑定依托自身索引的独立模型时,核心要解决滚动冲突、约束溢出、Widget复用时数据错位三个问题,具体实现步骤如下:
核心配置原则
- 内层列表必须禁用自身滚动,将滚动手势控制权完全交给外层列表,避免手势冲突导致的滑动卡顿、顿挫
- 内层列表开启高度自适应,解决垂直方向无界约束导致的布局溢出错误
- 内外层列表项都绑定和自身模型唯一标识对应的Key,避免Widget树复用时出现索引错位、状态串项问题
第一步:定义两层独立索引模型
先分别定义外层、内层绑定的模型类,每个外层模型持有对应自己的内层子模型列表:
// 外层列表绑定模型,依托外层列表索引 class ParentItemModel { final int parentId; // 外层项唯一标识 final String parentTitle; final List<ChildItemModel> childItems; // 对应内层子项数据 ParentItemModel({ required this.parentId, required this.parentTitle, required this.childItems, }); } // 内层列表绑定模型,依托内层列表索引 class ChildItemModel { final int childId; // 内层项唯一标识 final String content; final bool isChecked; ChildItemModel({ required this.childId, required this.content, required this.isChecked, }); }
第二步:完整实现代码
class NestedListPage extends StatelessWidget { NestedListPage({super.key}); // 模拟测试数据 final List<ParentItemModel> parentList = List.generate( 8, (outerIndex) => ParentItemModel( parentId: outerIndex, parentTitle: "分组 $outerIndex", childItems: List.generate( 3 + outerIndex % 4, // 每个分组子项数量不等,模拟真实业务场景 (innerIndex) => ChildItemModel( childId: innerIndex, content: "子条目 $outerIndex-$innerIndex", isChecked: innerIndex % 2 == 0, ), ), ), ); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("嵌套列表示例")), // 外层ListView,绑定外层索引与ParentItemModel body: ListView.builder( key: const PageStorageKey("outer_list_view"), padding: const EdgeInsets.all(12), itemCount: parentList.length, itemBuilder: (context, outerIndex) { final currentParent = parentList[outerIndex]; return Container( key: ValueKey("parent_${currentParent.parentId}"), margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( currentParent.parentTitle, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 6), // 内层ListView,绑定内层索引与ChildItemModel ListView.builder( // 核心配置1:禁用内层滚动,手势权交外层 physics: const NeverScrollableScrollPhysics(), // 核心配置2:自适应内容高度,解决无界约束溢出 shrinkWrap: true, itemCount: currentParent.childItems.length, itemBuilder: (context, innerIndex) { final currentChild = currentParent.childItems[innerIndex]; return ListTile( key: ValueKey("child_${currentParent.parentId}_${currentChild.childId}"), leading: Icon( currentChild.isChecked ? Icons.check_circle : Icons.radio_button_unchecked, color: currentChild.isChecked ? Colors.green : Colors.grey, ), title: Text(currentChild.content), onTap: () { // 可通过outerIndex、innerIndex精准定位两层模型处理交互 debugPrint("点击分组$outerIndex下的子项$innerIndex"); }, ); }, ) ], ), ); }, ), ); } }
注意事项
- 不要给内层列表写死高度:
shrinkWrap: true会自动计算所有子项的总高度适配,写死高度容易出现内容截断、多余留白的问题 - 不要保留内层列表的滚动能力:如果不设置
NeverScrollableScrollPhysics,内外层会抢滚动手势,出现滑动卡顿、列表拉不动的问题 - Key不要直接用列表索引:Key要绑定模型自身的唯一标识,否则列表增删、滚动复用时会出现子项状态错乱,比如选中状态串到其他条目上
- 单组内层子项超过50条时不建议用当前方案:
shrinkWrap: true会一次性渲染所有内层子项,内存占用高,这种场景建议换用CustomScrollView配合slivers嵌套实现,性能更好
内容的提问来源于stack exchange,提问作者Bhavik Dalal
相关产品推荐
相关产品推荐

