Saltar al contenido principal

Provider

Provider es el más básico de todos los providers. Crea un valor... Y eso es todo.

Provider se utiliza típicamente para:

  • almacenamiento de cálculos en caché
  • exponer un valor a otros providers (como un Repository/HttpClient).
  • ofreciendo una forma para testear o que los widgets sobrescriban un valor.
  • reduciendo las reconstrucciones de providers/widgets sin tener que usar select.

Uso de Provider para almacenar los cálculos en caché

Provider es una poderosa herramienta para almacenar en caché operaciones sincrónicas cuando se combina con ref.watch.

Un ejemplo sería filtrar una lista de tareas. Dado que filtrar una lista puede ser un poco costoso, idealmente no queremos filtrar nuestra lista de tareas cada vez que nuestra aplicación se vuelve a renderizar. En esta situación, podríamos usar Provider para hacer el filtrado por nosotros.

Para eso, suponga que nuestra aplicación tiene un StateNotifierProvider existente que manipula una lista de tareas:

class Todo {
Todo(this.description, this.isCompleted);
final bool isCompleted;
final String description;
}

class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);

void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO: añadir otros métodos, como "removeTodo", ...
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});

A partir de ahí, podemos usar Provider para exponer la lista filtrada de tareas, mostrando solo las tareas completadas:

final completedTodosProvider = Provider<List<Todo>>((ref) {
// Obtenemos la lista de tareas (todos) del `todosProvider`
final todos = ref.watch(todosProvider);

// Devolvemos solo los `todos` completados
return todos.where((todo) => todo.isCompleted).toList();
});

Con este código, nuestra interfaz de usuario ahora puede mostrar la lista de tareas completados al escuchar completedTodosProvider:

Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO: mostrar la lista de tareas (todos) en un ListView/GridView/...
});

La parte interesante es que el filtrado de listas ahora está en caché.

Lo que significa que la lista de tareas completados no se volverá a calcular hasta que se agreguen/eliminen/actualicen tareas, incluso si estamos leyendo la lista de tareas completadas varias veces.

Tenga en cuenta que no necesitamos invalidar manualmente el caché cuando cambia la lista de tareas. Provider es capaz de saber automáticamente cuándo se debe volver a calcular el resultado gracias a ref.watch.

Reducción de reconstrucciones de widgets/providers mediante el uso de Provider

Un aspecto único de Provider es que incluso cuando se vuelve a calcular Provider (normalmente cuando se usa ref.watch), no actualizará los widgets/providers que lo escuchan a menos que cambie el valor.

Un ejemplo del mundo real sería habilitar/deshabilitar los botones anterior/siguiente de una vista paginada:

Botón "Atrás/Siguiente"

En nuestro caso, nos centraremos concretamente en el botón "anterior". Una implementación ingenua de dicho botón sería un widget que obtiene el índice de la página actual, y si ese índice es igual a 0, deshabilitaríamos el botón.

Este código podría ser:

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

class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// Si no está en la primera página, el botón anterior está activo
final canGoToPreviousPage = ref.watch(pageIndexProvider) == 0;

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? null : goToPreviousPage,
child: const Text('previous'),
);
}
}

El problema con este código es que cada vez que cambiamos la página actual, el botón "anterior" se reconstruirá. En el mundo ideal, querríamos que el botón se reconstruya solo cuando cambie entre activado y desactivado.

La raíz del problema aquí es que estamos calculando si el usuario puede ir a la página anterior directamente desde el botón "anterior".

Una forma de resolver esto es extraer esta lógica fuera del widget y en un Provider:

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

// Un provider que calcula si el usuario puede ir a la página anterior
final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) == 0;
});

class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// Ahora estamos viendo nuestro nuevo Provider
// Nuestro widget ya no calcula si podemos ir a la página anterior.
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? null : goToPreviousPage,
child: const Text('previous'),
);
}
}

Al hacer esta pequeña refactorización, nuestro widget PreviousButton ya no se reconstruirá cuando el índice de la página cambie gracias a Provider.

A partir de ahora, cuando cambie el índice de la página, canGoToPreviousPageProvider se volverá a calcular. Pero si el valor expuesto por el provider no cambia, entonces PreviousButton no se reconstruirá.

Este cambio mejoró el rendimiento de nuestro botón y tuvo el beneficio interesante de extraer la lógica fuera de nuestro widget.