Saltar al contenido principal

StateProvider

StateProvider es un provider que expone una forma de modificar su estado. Es una simplificación de StateNotifierProvider, diseñada para evitar tener que escribir una clase StateNotifier para casos de uso muy simples.

StateProvider existe principalmente para permitir la modificación de variables simples por parte de la interfaz de usuario. El estado de un StateProvider suele ser uno de los siguientes:

  • un enum, como un tipo de filtro
  • un String, normalmente el contenido plano (raw) de un campo de texto
  • un boolean, para casillas de verificación
  • number, para paginación o campos de formulario de edad

No debe usar StateProvider si:

  • su estado necesita lógica de validación
  • su estado es un objeto complejo (como una clase personalizada, List/Map, ...)
  • la lógica para modificar su estado es más avanzada que un simple count++.

Para casos más avanzados, considere usar StateNotifierProvider en su lugar y cree una clase StateNotifier. Si bien el boilerplate será un poco más grande, tener una clase StateNotifier personalizada es fundamental para la capacidad de mantenimiento a largo plazo de su proyecto, ya que centraliza la lógica del negocio de su estado en un solo lugar.

Ejemplo de uso: Cambiar el tipo de filtro usando un menú desplegable (dropdown)

Un caso de uso del mundo real de StateProvider sería administrar el estado de componentes de formularios simples como dropdowns/text y fields/checkboxes.

En particular, veremos cómo usar StateProvider para implementar un menú desplegable que permita cambiar la forma en que se ordena una lista de productos.

En aras de la sencillez, la lista de productos que obtendremos se construirá directamente en la aplicación y será la siguiente:


class Product {
Product({required this.name, required this.price});

final String name;
final double price;
}

final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];

final productsProvider = Provider<List<Product>>((ref) {
return _products;
});

En una aplicación del mundo real, esta lista normalmente se obtendría utilizando FutureProvider al realizar una solicitud de red (network request).

La interfaz de usuario podría mostrar la lista de productos haciendo lo siguiente:


Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}

Ahora que hemos terminado con la base, podemos agregar un menú desplegable (dropdown), que permitirá filtrar nuestros productos ya sea por precio o por nombre.
Para eso, usaremos DropDownButton.

// Un enum que representa el tipo de filtro.
enum ProductSortType {
name,
price,
}

Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ...
),
);
}

Ahora que tenemos un dropdown, creemos un StateProvider y sincronicemos el estado del dropdown con nuestro provider.

Primero, vamos a crear el StateProvider:


final productSortTypeProvider = StateProvider<ProductSortType>(
// Devolvemos el tipo de clasificación predeterminado, aquí `name`.
(ref) => ProductSortType.name,
);

Luego, podemos conectar este provider con nuestro dropdown haciendo:

DropdownButton<ProductSortType>(
// Cuando cambia el tipo de clasificación, esto reconstruirá el dropdown
// para actualizar el icono que se muestra.
value: ref.watch(productSortTypeProvider),
// Cuando el usuario interactúa con el dropdown, actualizamos el estado del provider.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),

Con esto, ahora deberíamos poder cambiar el tipo de clasificación.
¡Aún no tiene impacto en la lista de productos! Ahora es el momento de la parte final: actualizar nuestro productsProvider para ordenar la lista de productos.

Un componente clave para implementar esto es usar ref.watch, para que nuestro productsProvider obtenga el tipo de clasificación y vuelva a calcular la lista de productos cada vez que cambie el tipo de clasificación.

La implementación sería:


final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});

¡Eso es todo! Este cambio es suficiente para que la interfaz de usuario vuelva a representar automáticamente la lista de productos cuando cambia el tipo de clasificación.

Aquí está el ejemplo completo en Dartpad:

Cómo actualizar el estado en función del valor anterior sin leer el provider dos veces

A veces, deseamos actualizar el estado de un StateProvider en función del valor anterior. Naturalmente, puedes terminar escribiendo:


final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Estamos actualizando el estado del valor anterior, terminamos leyendo
// el provider ¡dos veces!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}

Si bien no hay nada particularmente malo con este fragmento, la sintaxis es un poco inconveniente.

Para mejorar un poco la sintaxis, podemos usar la función update. Esta función recibirá un callback que recibirá el estado actual y se espera que devuelva el nuevo estado. Podemos usarlo para refactorizar nuestro código anterior a:


final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).update((state) => state + 1);
},
),
);
}
}

Este cambio logra el mismo efecto mejorando un poco la sintaxis.