Todos estamos acostumbrados a que para gestionar un estado sin ninguna librería en Flutter tenemos que crear un StatefulWidget. Y que para cambiar su estado y actualizar un widget tenemos que hacerlo mediante el método setState(() => state=newState)

Sin embargo, cada vez que ejecutamos setState estamos reconstruyendo todos los widgets pero debido a la eficacia con la que Flutter los reconstruye apenas notamos su coste. 

En algunos casos podemos minimizar los widgets que se reconstruyen extrayendo sólo el widget que queremos actualizar en un StatefulWidget.

Pero no en todos los casos es posible y en esos casos esto se consigue con la ayuda de ValueNotifier y ValueListenableBuilder.

ValueNotifier

ValueNotifier es una clase que extiende ChangeNotifier e implementa ValueListenable

En la clase ChangeNotifier está implementada toda la lógica de gestión de listeners. Te preguntarás ¿qué son los Listeners? Los listener son un listado de callbacks que se ejecutan cuando se llama la función notifyListeners.

Por lo que se podría decir que ValueNotifier es un ChangeNotifier que conserva un único valor nullable que puede ser de cualquier tipo y notifica a sus listeners cuando este valor cambia siempre y cuando el nuevo valor sea diferente al anterior.

ValueListenableBuilder

Si queremos escuchar las notificaciones del ValueNotifier necesitamos un ValueListenableBuilder.

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

ValueListenableBuilder es un StatefulWidget que actúa como builder y registra el método setState como listener en el Notifer mediante la interfaz del ValueListenable consiguiendo así ejecutar el método builder cada vez que se notifica un cambio de estado.

El tercer argumento child es opcional, hace referencia a un widget que necesitamos dentro del builder y queremos construir solo una vez.

Cosas a tener en cuenta:

  • El ValueListenableBuilder no gestiona si el valor del notifier es nulo o no así que si crees que el valor puede ser nulo tienes que controlarlo.
  • Cuando el ValueListenableBuilder ejecuta el método dispose se elimina de los listeners del Notifier.
  • Es recomendable que hagamos dispose del ValueNotifier pero si asignamos un nuevo valor después del dispose esto generará un FlutterError al ejecutar internamente notifyListeners.

Show me the code

Si tomamos como referencia el código del counter que nos aparece al crear un nuevo proyecto Flutter:

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++),
   );
 }
}

Implementado con el Value Notifier quedaría de esta manera:


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++,
   );
 }
}

Vale está bien pero ¿y si tenemos una lógica de negocio más compleja para el estado?

En estos casos, quizás necesites crear tu propio Notifier. Puedes conseguirlo extendiendo la clase ValueNotifier. Así:

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();
 }
}

En este ejemplo estamos creando un Value Notifier para evitar agregar un nuevo valor después del dispose en la respuesta de una consulta asíncrona.

Como puedes ver es bastante simple. En mi opinión, una forma no es mejor que la otra, todo dependerá del contexto.