ChangeNotifier shopping cart model
A small CartModel built on ChangeNotifier with add, remove, quantity and totals, wired into the UI through ListenableBuilder.
FlutterUtilitiesflutterstatechangenotifiercart
Code
import 'package:flutter/foundation.dart';
class CartLine {
CartLine({required this.id, required this.name, required this.unitCents, this.qty = 1});
final String id;
final String name;
final int unitCents; // money as integer minor units, never double
int qty;
int get totalCents => unitCents * qty;
}
class CartModel extends ChangeNotifier {
final Map<String, CartLine> _lines = {};
List<CartLine> get lines => List.unmodifiable(_lines.values);
int get itemCount => _lines.values.fold(0, (n, l) => n + l.qty);
int get subtotalCents => _lines.values.fold(0, (n, l) => n + l.totalCents);
bool get isEmpty => _lines.isEmpty;
void add(String id, String name, int unitCents) {
final existing = _lines[id];
if (existing != null) {
existing.qty++;
} else {
_lines[id] = CartLine(id: id, name: name, unitCents: unitCents);
}
notifyListeners();
}
void setQty(String id, int qty) {
final line = _lines[id];
if (line == null) return;
if (qty <= 0) {
_lines.remove(id);
} else {
line.qty = qty;
}
notifyListeners();
}
void clear() {
if (_lines.isEmpty) return;
_lines.clear();
notifyListeners();
}
}
import 'package:flutter/material.dart';
import 'cart_model.dart';
/// Rebuilds only this badge when the cart changes.
class CartBadge extends StatelessWidget {
const CartBadge({super.key, required this.cart, required this.child});
final CartModel cart;
final Widget child;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: cart,
child: child,
builder: (context, child) => Badge(
isLabelVisible: cart.itemCount > 0,
label: Text('${cart.itemCount}'),
child: child,
),
);
}
}
How it works
What it solves
Cart state is read from many places at once: an app bar badge, a product card stepper, a summary footer. Passing callbacks around gets tangled quickly, while a full state-management package is often more than a shop needs.
How it works
CartModel extends ChangeNotifier and owns a map of lines keyed by product id. Every mutator ends with notifyListeners(), and derived values such as itemCount and subtotalCents are computed getters, so they can never drift out of sync. Prices are integer minor units to avoid floating point rounding.
On the UI side, ListenableBuilder listens to the model and rebuilds only the builder subtree. The static child is passed through untouched, which keeps the icon from being recreated on each change.
Adapting it
Create one CartModel near the top of the tree and hand it down with an InheritedNotifier or your preferred provider package. Persist it by serialising lines after each notification. Add a maxQty guard in setQty if stock limits apply, or extend CartLine with variant data.
Pitfalls
linesreturns a copy of the values but theCartLineobjects are mutable; do not mutate them from widgets.- Dispose the model if you create it inside a
State. - Formatting cents to currency belongs in the view layer with
intl, not in the model.
How to use it
Place both files under lib/cart/. Create the model once, for example in main(), and share it.
final cart = CartModel();
CartBadge(cart: cart, child: const Icon(Icons.shopping_cart));
cart.add('p1', 'Apples', 349);
No packages are required.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.