Flutter State Management
Shopping Cart Application
- List items for shopping
- Click
Add
/‘Remove’ item in shopping - Show total number of added items in shopping card
Problem with current data structure
The first thing you need to do is to define a data structure to store your app’s state. To do that, we must determine what’s considers to be the app’s state
We know there are at least 2 components that require state: the product list and card icon.
ProductListState
: storeproductList
anditemsInCart
- If we implement by store locally of each state and keep in sync with
itemsInCart
then the problem may occur when the app getting complicated
class ProductListWidget extends StatefulWidget {
ProductListWidget({Key? key}): super(key: key);
@override
ProductListWidgetState createState() => ProductListWidgetState();
}
class ShoppingCartIconWidget extends StatefulWidget {
ShoppingCartIconWidget({Key? key}): super(key: key);
@override
ShoppingCartIconWidgetState createState() => ShoppingCartIconWidgetState();
}
class ProductListWidgetState extends State<ProductListWidget> {
List<String> get productList => _productList;
List<String> _productList = Server.getProductList();
set productList (List<String> value) {
setState(() {
_productList = value;
})
}
Set<String> get itemsInCart => _itemsInCart;
Set<String> _itemsInCart = <String>(); // Store locally Keep Sync with `_itemsInCart` of `ShoppingCartIconWidgetState`
set itemsInCart(Set<String> value) {
setState(() {
_itemInCart = value;
})
}
void _handleAddToCart(String id) {
itemsInCart = _itemsInCart..add(id);
// To keep in sync :(
shoppingCart.currentState!.itemsInCart = itemsInCart;
}
void _handleRemoveFromCart(String id) {
itemsInCart = _itemsInCart..remove(id);
// To keep in sync :(
shoppingCart.currentState!.itemsInCart = itemsInCart;
}
@override
Widget build(BuildContext context) {
return SomeUI();
}
}
class ShoppingCartIconWidgetState extends State<ShoppingCartIconWidget> {
Set<String> get itemsInCart => _itemsInCart;
Set<String> _itemsInCart = <String>(); // Store locally Keep Sync
set itemsInCart(Set<String> value) {
setState(() {
_itemsInCart = value;
})
}
@override
Widget build(BuildContext context) {
return SomeUI();
}
}
New data structure
- Create new data structure
class AppState {
AppState({required this.productList, this.itemsInCart = const <String>()});
AppState copyWith(List<String>? productList, Set<String>? itemsInCart) {
return AppState(productList: productList ?? this.productList, itemsInCart: itemsInCart ?? this.itemsInCart);
}
final List<String> productList;
final Set<String> itemsInCart;
}
-
Create an
InheritedWidget
Now we have own data structure to store the states. The next thing we want to do is to place the data above the widgets that need it, so entire widget subtree has access to the data. This is whereInheritedWidget
comes in handy. The widget has the ability to host data for the subtree and notify the subtree to rebuild when the data changes.The
InheritedWidget
itself does not have a state
class AppStateScope extends InheritedWidget {
AppStateScope(this.data, {Key? key, required Widget child}): super(key: key, child: child);
final AppState data;
static AppState of(BuildContext context) {
// This is convenient method subtree widget to get appstate from `context`
// This is magic method
// Try to find with type of `InheritedWidget` (in `AppStateScope` in this case) return the widget to you (Same the way give me a class type, I will return to you the instance)
// And notify InheritedWidget
return context.dependOnInheritedWidgetOfExactType<AppStateScope>()!.data; // can be null, then put ! before access property.
}
bool updateShouldNotify(AppStateScope oldWidget) {
// Boolean value to indicate the UI (incuding subtree widgets) will be rebuilt
return data == oldWidget.data
}
}
-
Create a StatefulWidget around the
AppStateScope
TheAppStateScope
does not have state, it only hosts the data that it receives. We still need to createStatefulWidget
to store the data. The goal of thisStatefulWidget
is to create theAppState
, provide APIs to modify the data, and host the data using theAppStateScope
-
Create
AppStateWidget
to wrap aroundAppStateScope
(becauseAppStateScope
itself does not have a state).
void main() {
runApp(AppStateWidget(
child: MaterialApp(
title: 'Store',
home: MyStorePage()
)
))
}
class AppStateWidget extends StatefulWidget {
AppStateWidget({required this.child});
final Widget child;
// Expose usefull API
static AppStateWidgetState of(BuildContext context) {
return context.findAncestorStateOfType<AppStateWidgetState>();
}
@override
AppStateWidgetState createState() => AppStateWidgetState();
}
// AppStateWidgetState refer to `AppState`
class AppStateWidgetState extends State<AppStateWidget> {
AppState _data = AppState(productList: Server.getProductList());
void setProductList(List<String> productList) {
if (_data.productList != productList) {
setState(() {
_data = _data.copyWith(productList: productList);
});
}
}
void addToCart(String id) {
if (!_data.itemsInCart.contains(id)) {
final Set<String> newItemsInCart = Set<String>.from(_data.itemsInCart)
setState(() {
// _data = _data.copyWith(itemsInCart: _data.itemsInCart.add(id)); // ERROR: same reference of `itemsInCart`
data = _data.copyWith(itemsInCart: newItemsInCart);
});
}
}
void removeToCart(String id) {
// Similar above
}
Widget build(BuildContext context) {
return AppStateScope(_data, child: widget.child)
}
}
flowchart
appState["<b>AppState</b><br>Data Model"]
appStateScope["<b>AppStateScope:InheritedWidget</b><br>Subtree Child can refer"]
appStateWidget["<b>AppStateWidget:StatefulWidget</b>"]
appStateWidgetState["<b>AppStateWidgetState</b>"]
subgraph " "
direction TB
appStateScope-.->appState
appStateWidget-.->appStateWidgetState
appStateWidgetState-.->appState
end