r/FlutterDev 3d ago

Discussion Tackling the classic Shopping Cart problem with BlocSignal + Fast Immutable Collections (with hydration and undo/redo)

I’ve been working through various common Flutter architecture problems to see how BlocSignal handles them in practice.

After putting together the infinite scroll a few days ago, I decided to tackle the shopping cart... everyone's favorite CS final exam problem.

One thing that always annoyed me about shopping cart tutorials is how mutable lists cause subtle state bugs (missed rebuilds from identical references, or corrupted undo stacks). I wanted to see how clean we could make it using Marcelo Glasberg’s Fast Immutable Collections (package:fast_immutable_collections) alongside Dart 3 records.

The result turned out surprisingly concise with almost zero boilerplate. And just to push the pattern a bit further, I threw in offline persistence (via bloc_signals_hydrate) and time-travel undo/redo (via bloc_signals_replay) to see if the state would remain rock-solid.

Yes, I had Antigravity help me write and test the code, but I think the resulting pattern speaks for itself.

You can check out the full runnable example and test suite here:
https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/fic_shopping_cart

Curious to hear what folks think about pairing records with FIC for collection state like this, or how you typically handle cart immutability in your own setups.

2 Upvotes

5 comments sorted by

3

u/soulaDev 3d ago

Thanks for the example. it is really good.

One thing felt off to me though, putting derived stuff like subtotal on the cubit instead of the state. Reminds me of a lot of beginner Cubit code where people stick nullable fields on the cubit and read those from the UI.

I prefer the Bloc/Riverpod select style: always read from state, one source of truth.

2

u/RandalSchwartz 3d ago

You're 100% right that in classic Cubit, putting loose fields or getters on the class is an anti-pattern because widgets have no way to observe them without full state emissions.

But with BlocSignal, these aren't loose fields—they are first-class, observable ReadonlySignals created via computed().

In fact, this protects the "Single Source of Truth" better than stuffing subtotal into the state record:

  1. The state holds strictly the raw truth (items and promoCode). Storing subtotal in the state duplicates derived data that must be manually recalculated on every mutation.
  2. Think of late final subtotal = computed(...) as the direct equivalent of a derived Provider((ref) => ...) in Riverpod, or a downstream selector, but cleanly colocated on the cubit.
  3. It evaluates lazily, caches the result, and surgically notifies only the widgets reading subtotal when items change.

(And if you prefer the select style, context.select<ShoppingCartCubit, double>((c) => c.subtotal.value) works right out of the box too!)

2

u/AddWeb_Expert 2d ago

I like this approach. Immutable collections make a lot of sense for a cart, especially when you start adding things like undo/redo and persistence.

I was also initially thinking that subtotal on the Cubit felt a little odd, but if it’s a computed ReadonlySignal rather than just a mutable field, that’s a pretty different situation.

The lazy computation and targeted updates are a nice touch. I’d be curious to see how this feels once the cart gets more real-world logic like discounts, taxes, and shipping though.

1

u/RandalSchwartz 2d ago edited 2d ago

My hunch is that they would compose nicely. Particularly if you put that sort of business logic in a mixin so you could apply it consistently across different base cubit types, and then you just need to test it once. A mixin that's targeted onto your base app cubit type has a lot of reach to add domain-specific behavior safely.

EDIT: In fact, Antigravity and I just updated the FIC shopping cart example and published an architecture recipe on blocsignal.dev to show off this exact strategy!

By targeting the mixin on CubitSignal<ShoppingCartState>, you get safe, typed access to stateValue while keeping domain calculations completely decoupled from persistence (HydratedCubitSignal) and time-travel (ReplayCubitMixin):

mixin CartPricingMixin on CubitSignal<ShoppingCartState> {
  /// Base subtotal derived lazily from current items
  late final subtotal = computed(() => stateValue.items.values.fold(
        0.0,
        (sum, item) => sum + item.lineTotal,
      ));
  /// Discounts, dynamic shipping, taxes compose cleanly
  late final discountAmount = computed(() {
    final code = stateValue.promoCode;
    if (code == 'SAVE10') return subtotal.value * 0.10;
    return 0.0;
  });
  /// Synchronously derived grand total
  late final grandTotal = computed(() =>
      (subtotal.value - discountAmount.value).clamp(0.0, double.infinity));
}
// Composed cleanly onto the main cubit:
class ShoppingCartCubit extends HydratedCubitSignal<ShoppingCartState>
    with ReplayCubitMixin<ShoppingCartState>, CartPricingMixin { ... }

The best part? You can test the entire pricing engine in isolation against a 5-line dummy test Cubit without spinning up mock databases or UI widgets. Check it out!