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

Flutter实现购物车条目动态更新的底部悬浮组件开发求助

解决方案:Flutter 校园点餐底部购物车组件实现

核心思路

放弃showBottomSheet的实现方式,改用Stack+Visibility将购物车组件固定在页面底部,通过轻量状态管理ChangeNotifier监听购物车数据变化,自动控制组件的显示/隐藏与内容更新。这种方式比showBottomSheet更稳定,避免页面切换时的状态丢失问题。

完整代码实现

1. 购物车数据模型与状态管理

import 'package:flutter/material.dart';

// 商品模型
class MenuItem {
  final String id;
  final String name;
  final double price;
  int quantity;

  MenuItem({
    required this.id,
    required this.name,
    required this.price,
    this.quantity = 1,
  });
}

// 购物车状态管理类
class CartProvider extends ChangeNotifier {
  final Map<String, MenuItem> _items = {};

  // 获取购物车商品列表
  List<MenuItem> get items => _items.values.toList();

  // 计算总金额
  double get totalAmount {
    var total = 0.0;
    _items.forEach((key, item) {
      total += item.price * item.quantity;
    });
    return total;
  }

  // 获取商品总数量
  int get itemCount => _items.values.fold(0, (sum, item) => sum + item.quantity);

  // 添加商品(数量+1或新增)
  void addItem(MenuItem item) {
    if (_items.containsKey(item.id)) {
      _items[item.id]!.quantity++;
    } else {
      _items[item.id] = MenuItem(
        id: item.id,
        name: item.name,
        price: item.price,
        quantity: 1,
      );
    }
    notifyListeners();
  }

  // 减少商品数量(数量为1时移除商品)
  void removeSingleItem(String itemId) {
    if (!_items.containsKey(itemId)) return;
    if (_items[itemId]!.quantity > 1) {
      _items[itemId]!.quantity--;
    } else {
      _items.remove(itemId);
    }
    notifyListeners();
  }

  // 直接移除商品
  void removeItem(String itemId) {
    _items.remove(itemId);
    notifyListeners();
  }

  // 清空购物车
  void clear() {
    _items.clear();
    notifyListeners();
  }
}

2. 餐厅菜单页面(含底部购物车)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'cart_provider.dart';
import 'cart_screen.dart';

class RestaurantMenuScreen extends StatelessWidget {
  // 模拟餐厅菜单数据
  final List<MenuItem> menuItems = [
    MenuItem(id: '1', name: '黄焖鸡米饭', price: 16.0),
    MenuItem(id: '2', name: '麻辣香锅', price: 22.0),
    MenuItem(id: '3', name: '兰州拉面', price: 12.0),
    MenuItem(id: '4', name: '台式卤肉饭', price: 18.0),
  ];

  RestaurantMenuScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('校园餐厅菜单')),
      body: ListView.builder(
        itemCount: menuItems.length,
        itemBuilder: (ctx, index) {
          final item = menuItems[index];
          return ListTile(
            title: Text(item.name),
            subtitle: Text('¥${item.price.toStringAsFixed(2)}'),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                // 减少商品按钮
                IconButton(
                  icon: const Icon(Icons.remove),
                  onPressed: () {
                    Provider.of<CartProvider>(context, listen: false)
                        .removeSingleItem(item.id);
                  },
                ),
                // 显示当前购物车中该商品的数量
                Consumer<CartProvider>(
                  builder: (ctx, cart, child) {
                    final targetItem = cart.items.firstWhere(
                          (cartItem) => cartItem.id == item.id,
                          orElse: () => MenuItem(id: '', name: '', price: 0),
                        );
                    return Text(targetItem.quantity > 0 ? targetItem.quantity.toString() : '0');
                  },
                ),
                // 添加商品按钮
                IconButton(
                  icon: const Icon(Icons.add),
                  onPressed: () {
                    Provider.of<CartProvider>(context, listen: false)
                        .addItem(item);
                  },
                ),
              ],
            ),
          );
        },
      ),
      // 底部购物车组件
      bottomNavigationBar: Consumer<CartProvider>(
        builder: (ctx, cart, child) {
          // 购物车为空时隐藏组件
          return Visibility(
            visible: cart.itemCount > 0,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
              height: 80,
              color: Colors.grey[100],
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  // 商品数量与总价展示
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        '共${cart.itemCount}件商品',
                        style: const TextStyle(fontSize: 14),
                      ),
                      Text(
                        '总计: ¥${cart.totalAmount.toStringAsFixed(2)}',
                        style: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                          color: Colors.red,
                        ),
                      ),
                    ],
                  ),
                  // 跳转购物车页面按钮
                  ElevatedButton(
                    onPressed: () {
                      Navigator.of(context).push(
                        MaterialPageRoute(builder: (ctx) => const CartScreen()),
                      );
                    },
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.red,
                      padding: const EdgeInsets.symmetric(
                        horizontal: 30,
                        vertical: 12,
                      ),
                    ),
                    child: const Text('去结算'),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

3. 购物车详情页面

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'cart_provider.dart';

class CartScreen extends StatelessWidget {
  const CartScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final cart = Provider.of<CartProvider>(context);
    return Scaffold(
      appBar: AppBar(title: const Text('我的购物车')),
      body: cart.items.isEmpty
          ? const Center(child: Text('购物车为空'))
          : ListView.builder(
              itemCount: cart.items.length,
              itemBuilder: (ctx, index) {
                final item = cart.items[index];
                return ListTile(
                  title: Text(item.name),
                  subtitle: Text('¥${item.price.toStringAsFixed(2)}'),
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.remove),
                        onPressed: () {
                          cart.removeSingleItem(item.id);
                        },
                      ),
                      Text(item.quantity.toString()),
                      IconButton(
                        icon: const Icon(Icons.add),
                        onPressed: () {
                          cart.addItem(item);
                        },
                      ),
                      IconButton(
                        icon: const Icon(Icons.delete),
                        onPressed: () {
                          cart.removeItem(item.id);
                        },
                      ),
                    ],
                  ),
                );
              },
            ),
      bottomNavigationBar: cart.items.isEmpty
          ? null
          : Container(
              padding: const EdgeInsets.all(15),
              height: 80,
              color: Colors.grey[100],
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    '总计: ¥${cart.totalAmount.toStringAsFixed(2)}',
                    style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      // 这里可对接后端下单接口
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('下单成功')),
                      );
                      cart.clear();
                      Navigator.of(context).pop();
                    },
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                    child: const Text('立即下单'),
                  ),
                ],
              ),
            ),
    );
  }
}

4. 项目入口配置(注入状态管理)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'cart_provider.dart';
import 'restaurant_menu_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (ctx) => CartProvider(),
      child: MaterialApp(
        title: '校园点餐',
        theme: ThemeData(primarySwatch: Colors.red),
        home: const RestaurantMenuScreen(),
      ),
    );
  }
}

关键功能说明

  • 实时更新:通过Consumer组件监听CartProvider的状态变化,购物车数量、总价会自动同步刷新
  • 自动显隐:利用Visibility组件判断cart.itemCount > 0,购物车为空时自动隐藏底部组件
  • 页面跳转:底部按钮通过Navigator.push跳转至购物车详情页,详情页同样支持商品增减、删除操作
  • 稳定性优势:相比showBottomSheet,固定底部的实现方式避免了弹窗状态管理的繁琐,适配点餐场景的高频交互

使用注意事项

  1. 需在pubspec.yaml中添加provider依赖:provider: ^6.0.5(可替换为最新版本)
  2. 模拟菜单数据可替换为真实接口返回的餐厅菜品数据
  3. 下单逻辑可根据需求对接校园点餐系统的后端接口

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 22:12:52