We are all used when managing a state without any library in Flutter we have to create a StatefulWidget. And that to change its state and update a widget we have to do it using the setState(() => state=newState) method.

However, every time we run setState we are rebuilding all the widgets but due to the efficiency with which Flutter rebuilds them, we hardly notice their cost.

In some cases we can minimize the rebuilt widgets by extracting only the widget we want to update in a StatefulWidget.

But not in all cases this is possible and in those cases this is achieved with the help of ValueNotifier and ValueListenableBuilder.

ValueNotifier

ValueNotifier is a class that extends ChangeNotifier and implements ValueListenable.

In the ChangeNotifier class all the listener management logic is implemented. You may ask yourself, what are listeners? The listeners are a list of callbacks that are executed when the notifyListeners function is called.

So you could say that ValueNotifier is a ChangeNotifier that keeps a single nullable value that can be of any type and notifies its listeners when this value changes as long as the new value is different from the previous one.

ValueListenableBuilder

If we want to listen the ValueNotifier notifications we need a ValueListenableBuilder.

class ValueListenableBuilder(
 @required valueListenable: ValueNotifier,
 @required builder: (context, value child) => Widget,
 child: Widget,
)

ValueListenableBuilder is a StatefulWidget that acts as a builder and registers the setState method as a listener in the Notifier through the ValueListenable interface, thus managing to execute the builder method every time a change of state is notified.

The third child argument is optional, it refers to a widget that we need inside the builder and we want to build only once.

Things to consider:

  • The ValueListenableBuilder does not manage if the value of the notifier is null or not so if you think that the value can be null you have to control it.
  • When the ValueListenableBuilder runs the dispose method removed itself from the Notifier listeners.
  • It is recommended that we make dispose of the ValueNotifier but if we assign a new value after the dispose this will generate a FlutterError when running internally notifyListeners.

Show me the code

If we take as a reference the counter code that appears when we create a new Flutter project:

class _CounterState extends State {
 int _counter = 0;

 @override
 Widget build(BuildContext context) {
   return AppScaffold(
     title: "Stateful Counter",
     children: [
       Text('You have pushed the button this many times:'),
       Text(
         '$_counter',
         style: Theme.of(context).textTheme.headline4,
       )
     ],
     onButtonPressed: () => setState(() => _counter++),
   );
 }
}

Implemented with the Value Notifier it would look like this:

class CounterNotifier extends StatelessWidget {
 final ValueNotifier _counter = ValueNotifier(0);

 @override
 Widget build(BuildContext context) {
   return AppScaffold(
     title: "ValueNotifier Counter",
     children: [
       Text('You have pushed the button this many times:'),
       ValueListenableBuilder(
           valueListenable: _counter,
           builder: (ctx, value, _) {
             return Text(
               '$value',
               style: Theme.of(context).textTheme.headline4,
             );
           })
     ],
     onButtonPressed: () => _counter.value++,
   );
 }
}

That’s fine, but what if we have a more complex business logic for the state?

In these cases, you may need to create your own Notifier. You can do this by extending the ValueNotifier class. How? Like this:

class CustomValueNotifier extends ValueNotifier {
 final SomeRepository repository;
 bool isDispose = false;

 CustomValueNotifier(int initValue, [this.repository = const SomeRepository()]) : super(initValue);

 void increment() async {
   int newValue = await this.repository.increment(value);
   value = newValue;
 }

 @override
 set value(int newValue) {
   if (!isDispose) super.value = newValue;
 }

 @override
 void dispose() {
   isDispose = true;
   super.dispose();
 }
}

In this example we are creating a Value Notifier to avoid adding a new value after the dispose in the answer of an asynchronous query.

As you can see it’s quite simple. In my opinion one way is not better than the other, it all depends on the context.