DEV3 Technologies home

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();
  }
}

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

  • lines returns a copy of the values but the CartLine objects 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.

Related snippets