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

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,
      };
}

关键修改点

  1. 数据拆分:获取商品数据后,用where方法将列表拆分为expensiveProducts和cheapProducts两组,便于单独渲染。
  2. UI结构重构:替换原ListView.builder为ListView,分别构建两组UI:
    • 先判断组内是否有商品,有则渲染标题
    • 用map遍历组内商品生成商品项
  3. 细节优化:
    • 添加dispose方法销毁StreamController,避免内存泄漏
    • 增加内边距优化排版
    • 用字符串插值替代拼接,代码更简洁

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 03:25:06