ValueListenableBuilder builder method not called after updating Notifier value

Viewed 207

I am trying to update the notifier value from parent widget whereas ValueListenableBuilder is defined in a child widget but the builder is not calling after changing the value.

Here is the parent widget code in which I have declared two child widgets as StatefulWidget and also declared a static object of Notifier class. I am calling the method updateMenuItemList from secondChild() widget like this HotKeysWidget.of(context)!.updateMenuItemList(currentCat!['items']); to update the list of firstChild() widget :

class HotKeysWidget extends StatefulWidget {
  static HotKeysWidgetState? of(BuildContext context) =>
      context.findAncestorStateOfType<HotKeysWidgetState>();

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return HotKeysWidgetState();
  }
}

class HotKeysWidgetState extends State<HotKeysWidget> {
static DealsNotifier appValueNotifier = DealsNotifier();

updateMenuItemList(List<Food> list) {
    appValueNotifier.updateMenuList(list);
}

 @override
  Widget build(BuildContext context) {
  return Container(child: Column(children: [
  firstChild(),
  secondChild(),
  ],
  ),
  );
  
}

}

Here is my Notifier class:

class DealsNotifier {
  ValueNotifier<List<Food>> dealList = ValueNotifier([]);
  ValueNotifier<List<Food>> menuitemList = ValueNotifier([]);
  ValueNotifier<List<Map<String,dynamic>>> categoryList = ValueNotifier([]);

  void updateDealsList(List<Food> list) {
    dealList.value = list;
    print('DEAL LIST IN CLASS: ${dealList}');
  }

  void updateMenuList(List<Food> list) {
    menuitemList.value = list;
    print('PRICE CHANGE: ${menuitemList.value[2].price}');
    print('MENU ITEM LIST IN CLASS: ${menuitemList}');
  }

  void updateCategoryList(List<Map<String,dynamic>> catList) {
    categoryList.value = catList;
    print('DEAL LIST IN CLASS: ${categoryList}');
  }
  List<Food> getDealList() {
    return dealList.value;
  }

  List<Food> getMenuitemList() {
    return menuitemList.value;
  }

  List<Map<String,dynamic>> getCategoryList() {
    return categoryList.value;
  }
}

And this is the child widget named as firstChild() in parent code. Here the ValueListenerBuilder is declared:

class firstChild extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return firstChildState();
  }

}

class firstChildState extends State<firstChild> {

@override
  Widget build(BuildContext context) {
  return ValueListenableBuilder(
    valueListenable: HotKeysWidgetState.appValueNotifier.menuitemList,
builder: (context, List<Food> value, widget)
{
   print('MENUITEM LIST UPDATED: ${value}');
   return HotkeysMenuItemsWidget(
   key: menuItemsKey,
   currentMenu:currentCat != null ? value : [],
   );
},
);

}
}

class secondChild extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return secondChildState();
  }

}

class secondChildState extends State<secondChild> {

@override
  Widget build(BuildContext context) {
  return RaisedButton(
                    
                    onPressed: (){
                    HotKeysWidget.of(context)!.updateMenuItemList([]);
                    },
                    child: Text(
                      'UPDATE',
                      maxLines: 2,
                      textAlign: TextAlign.center,
                      style: const TextStyle(
                        fontSize: 12,
                      ),
                    ),
                  );

}
}

Anyone help me with this issue please.

Thanks in advance

1 Answers

While there's still not enough code shared to fully reproduce your situation, I can offer some suggestions.

The state portion of StatefulWidgets are private by default for a reason. You shouldn't make them public just to access variables that are inside there are several other to access outside classes within widgets.

So anytime you're doing something like this

class firstChild extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return firstChildState();
  }
}

class firstChildState extends State<firstChild> {
  @override
...

Just stick to the default syntax of a StatefulWidget and also classes should be in UpperCamelCase with the first letter capitalized.

class FirstChild extends StatefulWidget {
  const FirstChild({Key? key}) : super(key: key);

  @override
  State<FirstChild> createState() => _FirstChildState();
}

class _FirstChildState extends State<FirstChild> {
  @override
  Widget build(BuildContext context) {
...

If you find yourself wanting to edit this default syntax its a clue that you need to find a better way to achieve whatever it is you're trying to do. If you're need to access a function that is declared in a Widget from outside that Widget, then that function should be declared somewhere else.

All that being said, unless you need setState, initState or another of the lifecycle functions, then you don't need a StatefulWidget to begin with. All those classes can be Stateless.

An easy way to make that DealsNotifier class globally accessible without a full on state management solution is to make it a static class.


class DealsNotifier {
  static ValueNotifier<List<Food>> dealList = ValueNotifier([]);
  static ValueNotifier<List<Food>> menuitemList = ValueNotifier([]);
  static ValueNotifier<List<Map<String, dynamic>>> categoryList =
      ValueNotifier([]);

  static void updateDealsList(List<Food> list) {
    dealList.value = list;
    print('DEAL LIST IN CLASS: ${dealList}');
  }

  static void updateMenuList(List<Food> list) {
    menuitemList.value = list;
    print('PRICE CHANGE: ${menuitemList.value[2].price}');
    print('MENU ITEM LIST IN CLASS: ${menuitemList}');
  }

  static void updateCategoryList(List<Map<String, dynamic>> catList) {
    categoryList.value = catList;
    print('DEAL LIST IN CLASS: ${categoryList}');
  }

  static List<Food> getDealList() {
    return dealList.value;
  }

  static List<Food> getMenuitemList() {
    return menuitemList.value;
  }

  static List<Map<String, dynamic>> getCategoryList() {
    return categoryList.value;
  }
}

Then when you need to pass in the valueListenable you access via DealsNotifier.menuitemlist and its always the same instance.

return ValueListenableBuilder(
      valueListenable: DealsNotifier.menuitemList,
      builder: (context, List<Food> value, widget) {
        print('MENUITEM LIST UPDATED: ${value}');
        return HotkeysMenuItemsWidget(
          key: menuItemsKey,
          currentMenu: currentCat != null ? value : [],
        );
      },
    );

Here's the Stateless version of all those classes and wherever you need the UI update you can use ValueListenableBuilder and pass in DealsNotifier.whicheverVariableYouWantToListenTo in the valueListenable. Then call whichever relevant method from the DealsNotifier class ie. DealsNotifier.updateMenuList([]).

And you didn't share your HotkeysMenuItemsWidget but if that's where you're looking to see the change in the UI, then that is where the ValueListenableBuilder should be. Its currently too high up in the widget tree all it needs to do is re-render the list in that Widget, you don't need/want an entire re-build of the HotkeysMenuItemsWidget from a parent widget.


class FirstChild extends StatelessWidget {
  const FirstChild({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder( // this should be inside HotkeysMenuItemsWidget
      valueListenable: DealsNotifier.menuitemList,
      builder: (context, List<Food> value, widget) {
        print('MENUITEM LIST UPDATED: ${value}');
        return HotkeysMenuItemsWidget(
          key: menuItemsKey,
          currentMenu: currentCat != null ? value : [],
        );
      },
    );
  }
}

class SecondChild extends StatelessWidget {
  const SecondChild({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        DealsNotifier.updateMenuList([]); 
      },
      child: Text(
        'UPDATE',
        maxLines: 2,
        textAlign: TextAlign.center,
        style: const TextStyle(
          fontSize: 12,
        ),
      ),
    );
  }
}

class HotKeysWidget extends StatelessWidget {
  const HotKeysWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          FirstChild(),
          SecondChild(),
        ],
      ),
    );
  }
}
Related