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: store productList and itemsInCart
  • 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 where InheritedWidget 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 The AppStateScope does not have state, it only hosts the data that it receives. We still need to create StatefulWidget to store the data. The goal of this StatefulWidget is to create the AppState, provide APIs to modify the data, and host the data using the AppStateScope

  • Create AppStateWidget to wrap around AppStateScope (because AppStateScope 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