“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).