How does one combine mutable and immutable information?

I have an application where I’m dealing with a list of LineItem widgets. Each widget displays a product name and price (immutable) and the quantity (mutable). The quantity is held as an integer in a State<LineItem> subclass, and when the quantity changes, the quantity and line total parts of the LineItem update as expected - all well and good.

However, I also want an “order total” widget to show the sum of all the lines, and to do this I need information which is partly held in the widget (price) and partly in the state (quantity). I see that a State can access its widget through the widget property, but what’s the way of reading the state from the widget? It’s perhaps a dumb question, but the resources I’ve looked at mention setState aplenty – but where’s getState? Is there a way to access that information?

I’ve set up a ListenableBuilder for the order total widget and I know it’s being updated whenever any quantity changes, but I can’t see how to actually compute the order total to display it.

You might need a callback that runs when the quantity changes and provides the quantity as an argument.

You can then use the provided argument to modify the quantity where you want.

Could you share some code? That would make it easier to solve this.

Could you share some code?

I’ll see what I can do to pare it down to a minimal example, but it might take a bit of work.

I did think about something like a more flexible signalling approach (the ChangeNotifier design seems curiously hobbled), but was trying to achieve this the “official” way.

1 Like

It would be easier if your state has this structure:

final class _LineItemRow {
  Product product;
  int quantity;
}

(i.e.: create a class to hold both product and quantity).

You could also just use a Map<Product, int>{};

This ensures not duplicated products (if they are value equatable, I recommend using dart_mappable for that) and you can get the list of products and quantities with map.entries (each entry has a Key (Product) and a Value (quantity).

It is also easier to search for products, delete them (from the state, I’m assuming it is some kind of shopping cart), etc.

but was trying to achieve this the “official” way

There is no such thing!

Yes, I’d done that (or something like it). But since mutable stuff is supposed to be in the state, don’t you have to pass a Product instance into the state constructor? That gives complaints about “avoid logic in state” - does one just ignore those?

Even if one passes a Product in, there’s no easier way for a separate widget to get hold of the quantity or line amount (which is inside the state) which would be needed to get the order total. I suppose one can use a callback…

Glad to hear it!

“avoid logic in state”

There is no such thing as “state”. State is not a thing that you have in your application, physically. State means the current state of your application. If your application is an offline app with a SQLite database, your state is the current data on your database. If you are building a form or a shopping cart inside a “page”, this is your state for that case.

In Flutter, people use MVVM (BLoC, Riverpod, ChangeNotifier, etc. are MVVM) or MVC (including me, with CQRS/Mediator Pattern). In this context, there ain’t much difference between MVVM and MVC: you get a Model (your shopping cart, with links do Domain Entities (products)) and you manipulate it in your Model (ex.: a method in ChangeNotifier) or in your Controller (ex.: a CommandHandler in your Mediator). Somehow (in MVVM, it’s a ListenableBuilder, triggered by ChangeNotifier notifyListeners()), the view will be updated whenever this model (or ViewModel) changes.

So, basically:

final class ShoppingCart extends ChangeNotifier {
  ShoppingCart({required Map<String, double> productPrices}) : _productPrices = productPrices;
  
  final Map<String, double> _productPrices;
  final _items = <String, int>{}; // ProductId and Quantity
  
  Map<String, int> get items => _items; // Only local methods can alter this
  
  double _totalPrice = 0;
  double get totalPrice => _totalPrice;
  
  void addProduct(String productId, int quantity) {
    final currentQuantity = _items[productId] ?? 0;
    
    _items[productId] = currentQuantity + quantity;
    _totalPrice += _productPrices[productId]! * quantity; // unsafe, it's just an example
    notifyListeners(); // rebuild UI
  }
  
  void removeProduct(String productId) {
    final removedQuantity = _items.remove(productId);
    
    _totalPrice -= _productPrices[productId]! * removedQuantity; // unsafe
    notifyListeners(); // rebuild UI
  }
  
  void clearCart() {
    _items.clear();
    _totalPrice = 0;
    notifyListeners(); // rebuild UI
  }
}

Put one of this in your local state (StatefulWidget - notice that it is a LOCAL STATE, it has nothing to do with your APPLICATION STATE), use the methods to alter your local state and envelop your UI on a ListenerBuilder (so that notifyListeners() will trigger a rebuild.

The ChangeNotifier here is a ViewModel and it is responsible to deal with that local state. It is the only piece of code that can change your local state (notice how the _items is read-only for public access). It exists only in this page (hence, local state).

This will become an APPLICATION state when you submit this cart to do something (like a purchase or something). In that case, write another method in that ViewModel like submitPurchase(). That will send the current shopping cart to your backend and then it will change your application state. This is where those state managements come into play: a huge change like this could potentially change your entire UI, so those managers will listen to something and rebuild the widgets properly (for instance: this would clear the shopping cart, show the new order in the user’s orders, list, etc.). For those cases, for me, personally, I prefer to work with domain events. Something happened in my domain (OrderCreated) and I would just emit an event and all parts that are interest in this event will respond to it. I like clean solutions that don’t mixes things together (which is a really common problem for BLoC, for instance: lots of users keep asking what if one bloc needs another bloc? In my case, I just have messages, so, I just emit a message when I need something (a query (read-only), a command (changes something) or an event (notifies that something has happened). I personally like this package: streamline | Flutter package (check the example).

The trick now is: this page of yours have all you need to deal with this local state? If not, then you should pass this ChangeNotifier to the widgets that deals with it. This can be done by passing it as a ctor argument (simple enough for simple cases, such as:)

final _viewModel = ShoppingCart();

@override
void dispose() {
  super.dispose();
  _viewModel.dispose(); // Important
}

Widget build(BuildContext context) {
  return Scaffold( // my shopping cart page
    body: ShoppingCartList(viewModel: _viewModel);
   ...
  );
}

Since the view will rebuild with notifyListeners() you don’t have ever to call setState() (which really should be named rebuildView(), it would avoid sooooo much confusion).

But, if your widget tree is more chaotic or if you need this ShoppingCart thing in places you don’t even expect yet, then you could just added to the Widgets tree and make it available anywhere (just like Theme and MediaQuery are).

For this, you would use an InheritedWidget and use the context.dependOnInheritedWidgetOfExactType<YourInheritedWidget>()!.shoppingCart to retrieve (just like you do with Theme.of(context).

If you think writing your InheritedWidgets is cumbersome, use a helper, like Provider (it will abstract InheritedWidget for you and works just fine with ChangeNotifiers).

4 Likes

Thanks @evaluator118 for taking so much time to explain things with so much example code - much appreciated.

I have got things working in a slightly different way, and I had to do a couple of things which if I were being uncharitable I would call hacks, because the framework doesn’t make some things straightforward. At some point I will write a post about it.

One thing I’ve taken away from the exercise is that there is seemingly no good general-purpose library for signalling events flexibly between different parts of the framework. Things like ChangeNotifier, ListenableBuilder, StatefulBuilder are well and good but very rigid in how you can use them. I ended up not using StatefulBuilder but did use the other two, as well as callbacks. It’s not exactly a shopping cart application, more a simple point-of-sale/visitor management system for a ticket desk at an event I help manage.

It’s not too large an app, coming in at under 400 lines (so far).

You might want to have a look at watch_it .

3 Likes

And watch what?

You need to watch something (and this is often a ChangeNotifier).

Tools does not replace functionality, they are only tools… they help.

(btw, watch_it has a very nice helper that is watch a property of a class, instead of the entire class, useful for granular rebuilds, but you’ll still need the class itself (and I think it MUST be ChangeNotifier, not sure)).

1 Like

Watch with watch_it whatever the OP was using ListenableBuilder for and found rigid in the way it’s used. watch_it watches listenables in a very flexible way.
@escamoteur (the watch_it developer) also wrote an article about using a proxy object that might be a possible approach, too: