Kodokon kodokon.com

Gestión de estado estructurada: ChangeNotifier y Provider

Comparte el estado de la aplicación con ChangeNotifier y Provider mientras dominas watch, read y select para reconstrucciones específicas.

10 min · 3 preguntas

Abrir esta lección en Kodokon

setState basta mientras el estado vive y muere con una sola pantalla. En cuanto los datos se comparten (carrito, sesión, preferencias), pasarlos de constructor en constructor se vuelve inmanejable. ChangeNotifier proporciona el observable mínimo del SDK; provider lo inyecta en el árbol y reconstruye solo los widgets suscritos. Frente a Riverpod o Bloc, esta pareja sigue siendo la más fácil de auditar: poca magia, una única dependencia, un mecanismo que puedes explicar en una sola frase.

BASH
flutter pub add provider

Un notifier bien diseñado encapsula: campos privados, getters de solo lectura, mutaciones únicamente a través de métodos que terminan con notifyListeners(). Ese contrato es lo que garantiza que ningún cambio de estado se escape de la interfaz.

DART
import 'package:flutter/foundation.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);

  int get count => _items.length;

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }

  void remove(String item) {
    _items.remove(item);
    notifyListeners();
  }
}
Estado privado, de solo lectura hacia el exterior, notificación.

Tres puntos de entrada, tres usos. context.watch<T>() suscribe el widget: se reconstruye con cada notifyListeners. context.read<T>() lee sin suscribir: es la herramienta para callbacks como onPressed. context.select<T, R>() se suscribe solo a una proyección: el widget se reconstruye únicamente si ese valor concreto cambia. Regla de oro: watch y select solo en build, read en los manejadores; nunca al revés.

DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int _value = 0;

  int get value => _value;

  void increment() {
    _value++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const CounterApp(),
    ),
  );
}

class CounterApp extends StatelessWidget {
  const CounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    final value = context.watch<CounterModel>().value;
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Count: $value')),
        floatingActionButton: FloatingActionButton(
          onPressed: () =>
              context.read<CounterModel>().increment(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
watch en build para leer, read en el callback para actuar.
DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  int get count => _items.length;
}

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select(
      (CartModel cart) => cart.count,
    );
    return Badge(
      label: Text('$count'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}
CartBadge solo se reconstruye cuando count cambia.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. Dentro de un onPressed, ¿cómo activas cart.add(...) sin crear una suscripción innecesaria?
    • context.watch<CartModel>().add(...)
    • context.read<CartModel>().add(...)
    • context.select((CartModel c) => c.add(...))
  2. ¿Qué desencadena exactamente notifyListeners()?
    • La reconstrucción únicamente de los widgets que usan los datos que cambiaron.
    • Una reconstrucción completa de la aplicación.
    • La reconstrucción de todos los widgets suscritos al notifier, sin importar qué dato cambió.
  3. ¿Por qué exponer List.unmodifiable(_items) en lugar de _items directamente?
    • Para mejorar el rendimiento de lectura de la lista.
    • Para impedir una mutación externa que cambiaría el estado sin notifyListeners y, por tanto, sin reconstrucción.
    • Porque Provider se niega a inyectar listas mutables.