Flutter如何实现ListView按条件分组且分组标题仅显示一次
Flutter商品分组展示:解决重复显示分组标题的问题
我需要将商品按价格分为「昂贵(Expensive)」(价格≥100)和「便宜(Cheap)」(价格<100)两组展示,每组的标题容器仅显示一次。但当前实现的代码会随每个商品重复显示标题容器,尝试用bool变量控制显示状态也没成功,附上简化代码寻求修改方案。
我的商品列表:
List<ProductModel> productList = [ ProductModel(name: "Oranges", price: 150), ProductModel(name: "Apples", price: 80), ];
原简化代码
class ProductListPage extends StatefulWidget { @override State<StatefulWidget> createState() => new _ProductListPageState(); } class _ProductListPageState extends State<ProductListPage> { late StreamController<int> _postsController; List<ProductModel> productList = []; @override void initState() { _postsController = new StreamController(); getProducts(); super.initState(); } void getProducts() { Fetch.getProdcuts().then((value) => { _postsController.add(1), productList.addAll(value!) }); } @override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( child: Column( children: <Widget>[ Container( child: Column( children: <Widget>[ const Text("商品列表"), ]), ), products(), ], ), ), ); } Widget products() { return StreamBuilder<int>( stream: _postsController.stream, builder: (BuildContext context, AsyncSnapshot<int> snapshot) { if (snapshot.hasData) { return productsView(); } else { return Center( child: Text('暂无商品'), ); } }); } Widget productsView() { return ListView.builder( shrinkWrap: true, itemCount: productList.length, physics: NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final p = productList[index]; return Container( child: Column( children: <Widget>[ Container( // 希望这个容器只显示一次 child: const Text('昂贵'), ), p.price! >= 100 ? Container( child: Text('${p.name!}: \$${p.price}'), ) : Container(), const SizedBox( // 希望这个间距只显示一次 height: 30.0, ), Container( // 希望这个容器只显示一次 child: const Text('便宜'), ), p.price! < 100 ? Container( child: Text('${p.name!}: \$${p.price}'), ) : Container(), ]), ); }); } } class ProductModel extends Object { String? name; double? price; ProductModel({this.name, this.price}); ProductModel.fromJson(Map<String, dynamic> json) { name = json["name"]; price = json["price"]; } Map<String, dynamic> toJson() => { 'name': name, 'price': price, }; }
修改方案
核心思路
原代码的问题在于每个商品item都重复渲染了两组标题,正确做法是先将商品列表拆分为两组,再分别为每组构建「标题+商品项」的独立结构,确保每组标题仅渲染一次。
修改后的完整代码
class ProductListPage extends StatefulWidget { @override State<StatefulWidget> createState() => _ProductListPageState(); } class _ProductListPageState extends State<ProductListPage> { late StreamController<int> _postsController; List<ProductModel> productList = []; // 拆分后的分组列表 late List<ProductModel> expensiveProducts; late List<ProductModel> cheapProducts; @override void initState() { _postsController = StreamController(); getProducts(); super.initState(); } void getProducts() { Fetch.getProdcuts().then((value) { setState(() { productList.addAll(value!); // 拆分商品为两组 expensiveProducts = productList.where((p) => p.price! >= 100).toList(); cheapProducts = productList.where((p) => p.price! < 100).toList(); }); _postsController.add(1); }); } @override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( child: Column( children: <Widget>[ const Padding( padding: EdgeInsets.all(16.0), child: Text("商品列表", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), ), products(), ], ), ), ); } Widget products() { return StreamBuilder<int>( stream: _postsController.stream, builder: (BuildContext context, AsyncSnapshot<int> snapshot) { if (snapshot.hasData) { return productsView(); } else { return const Center(child: Text('暂无商品')); } }, ); } Widget productsView() { return ListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ // 昂贵商品组 if (expensiveProducts.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text('昂贵', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ), ...expensiveProducts.map((p) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Text('${p.name!}: \$${p.price}'), )).toList(), const SizedBox(height: 30), ], ), // 便宜商品组 if (cheapProducts.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text('便宜', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ), ...cheapProducts.map((p) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Text('${p.name!}: \$${p.price}'), )).toList(), ], ), ], ); } @override void dispose() { _postsController.close(); // 销毁StreamController避免内存泄漏 super.dispose(); } } class ProductModel extends Object { String? name; double? price; ProductModel({this.name, this.price}); ProductModel.fromJson(Map<String, dynamic> json) { name = json["name"]; price = json["price"]; } Map<String, dynamic> toJson() => { 'name': name, 'price': price, }; }
关键修改点
- 数据拆分:获取商品数据后,用
where方法将列表拆分为expensiveProducts和cheapProducts两组,便于单独渲染。 - UI结构重构:替换原
ListView.builder为ListView,分别构建两组UI:- 先判断组内是否有商品,有则渲染标题
- 用
map遍历组内商品生成商品项
- 细节优化:
- 添加
dispose方法销毁StreamController,避免内存泄漏 - 增加内边距优化排版
- 用字符串插值替代拼接,代码更简洁
- 添加
内容的提问来源于stack exchange,提问作者BradG
相关产品推荐
相关产品推荐

