Flutter Provider: How to notify a model that a change happened on a model it contains?

3,400

this ist not a nested Provider, but i think in your example it is the better way..

only one ChangeNotifierProvider per section ("Frozen", "Fruits and Veggies") is defined

the complete() function from a ItemModel is in the GroceryListSectionModel() and with the parameter from the current List Index

class GroceryListSection extends StatelessWidget {
  final GroceryListSectionModel model;
 // final ValueChanged<bool> onChanged;

  GroceryListSection(this.model);

  @override
  Widget build(BuildContext context) {

    return new ChangeNotifierProvider<GroceryListSectionModel>(
        create: (context) => GroceryListSectionModel(model.name, model.items),
        child: new Consumer<GroceryListSectionModel>(
        builder: (context, groceryListSection, child) {
          return Container(
              child: ExpansionTile(
                  title: Text(model.name),
                  subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
                  children: groceryListSection.items.asMap().map((i, groceryItemModel) => MapEntry(i, GroceryItem(groceryItemModel, i))).values.toList()
              )
          );
        }
      )
    );
  }
}

class GroceryItem extends StatelessWidget {
  final GroceryItemModel model;
  final int index;

  GroceryItem(this.model, this.index);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(model.name),
      leading: model.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked),
      onTap: () => Provider.of<GroceryListSectionModel>(context, listen: false).complete(index),
    );

  }
}

class GroceryListSectionModel extends ChangeNotifier {
  String name;
  List<GroceryItemModel> items;

  GroceryListSectionModel(this.name, [items]) {
    this.items = items == null ? [] : items;
  }

  int itemCount() => items.length;
  int completedItemCount() => items.where((item) => item.completed).length;

  // complete Void with index from List items
  void complete(int index) {
    this.items[index].completed = true;
    notifyListeners();
  }

}

// normal Model without ChangeNotifier
class GroceryItemModel {
  final String name;
  bool completed = false;

  GroceryItemModel({this.name, completed}) {
    this.completed = completed == null ? false : completed;
  }

}
Share:
3,400
DogsGoQuack
Author by

DogsGoQuack

Updated on December 12, 2022

Comments

  • DogsGoQuack
    DogsGoQuack over 1 year

    I'm starting to learn Flutter/Dart by building a simple Todo app using Provider, and I've run into a state management issue. To be clear, the code I've written works, but it seems... wrong. I can't find any examples that resemble my case enough for me to understand what the correct way to approach the issue is.

    This is what the app looks like

    It's a grocery list divided by sections ("Frozen", "Fruits and Veggies"). Every section has multiple items, and displays a "x of y completed" progress indicator. Every item "completes" when it is pressed.

    TheGroceryItemModel looks like this:

    class GroceryItemModel extends ChangeNotifier {
      final String name;
    
      bool _completed = false;
    
      GroceryItemModel(this.name);
    
      bool get completed => _completed;
    
      void complete() {
        _completed = true;
        notifyListeners();
      }
    }
    

    And I use it in the GroceryItem widget like so:

    class GroceryItem extends StatelessWidget {
      final GroceryItemModel model;
    
      GroceryItem(this.model);
    
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider.value(
            value: model,
            child: Consumer<GroceryItemModel>(builder: (context, groceryItem, child) {
              return ListTile(
                  title: Text(groceryItem.name),
                  leading: groceryItem.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked)
                  onTap: () => groceryItem.complete();
            })
        );
      }
    }
    

    The next step I want is to include multiple items in a section, which tracks completeness based on how many items are completed.

    The GroceryListSectionModel looks like this:

    class GroceryListSectionModel extends ChangeNotifier {
      final String name;
      List<GroceryItemModel> items;
    
      GroceryListSectionModel(this.name, [items]) {
        this.items = items == null ? [] : items;
    
        // THIS RIGHT HERE IS WHERE IT GETS WEIRD
        items.forEach((item) {
          item.addListener(notifyListeners);
        });
        // END WEIRD
      }
    
      int itemCount() => items.length;
      int completedItemCount() => items.where((item) => item.completed).length;
    }
    

    And I use it in the GroceryListSection widget like so:

    class GroceryListSection extends StatelessWidget {
      final GroceryListSectionModel model;
      final ValueChanged<bool> onChanged;
    
      GroceryListSection(this.model, this.onChanged);
    
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider.value(
          value: model,
          child: Consumer<GroceryListSectionModel>(
            builder: (context, groceryListSection, child) {
              return Container(
                  child: ExpansionTile(
                      title: Text(model.name),
                      subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
                      children: groceryListSection.items.map((groceryItemModel) =>
                          GroceryItem(groceryItemModel)).toList()
                  )
              );
            }
          )
        );
      }
    }
    

    The Problems:

    1. It seems weird to have a ChangeNotifierProvider and a Consumer in both Widgets. None of the examples I've seen do that.
    2. It's definitely wrong to have the GroceryListSectionModel listening to changes on all the GroceryItemModels for changes to propagate back up the tree. I don't see how that can scale right.

    Any suggestions? Thanks!