Saltar al contenido principal

Leyendo un Provider

Antes de leer esta guía, asegúrese de leer primero acerca de los providers.

En esta guía, veremos cómo consumir un provider.

Obtener un objeto "ref"

En primer lugar, antes de leer un provider, necesitamos obtener un objeto "ref".

Este objeto es el que nos permite interactuar con los providers, ya sea desde un widget o desde otro provider.

Obtener un "ref" de un provider

Todos los providers reciben un "ref" como parámetro:

final provider = Provider((ref) {
// use ref para obtener otros providers
final repository = ref.watch(repositoryProvider);

return SomeValue(repository);
})

Es seguro pasar este parámetro al valor expuesto por el provider.

Por ejemplo, un caso de uso común es pasar la "ref" del provider a un StateNotifier:

final counter = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});

class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);

final Ref ref;

void increment() {
// `Counter` puede usar la "ref" para leer otros providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

Hacerlo permitiría que nuestra clase Counter lea providers.

Obtener un objeto "ref" de un widget

Los widgets, naturalmente, no tienen un parámetro ref. Pero Riverpod ofrece múltiples soluciones para obtener uno de los widgets.

Extendiendo de ConsumerWidget en lugar de StatelessWidget

La solución más común será reemplazar StatelessWidget por ConsumerWidget al crear un widget.

ConsumerWidget es básicamente idéntico a StatelessWidget, con la única diferencia de que tiene un parámetro adicional en su método build: el objeto "ref".

Un ConsumerWidget típico se vería así:

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

@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref para escuchar un provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Extendiendo de ConsumerStatefulWidget+ConsumerState en lugar de StatefulWidget+State

Al igual que ConsumerWidget, ConsumerStatefulWidget y ConsumerState son el equivalente de un StatefulWidget con su State (estado), con la diferencia de que el estado tiene un objeto "ref".

Esta vez, la "ref" no se pasa como un parámetro del método de build, sino que es una propiedad del objeto ConsumerState:

class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);

@override
HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" se puede utilizar en todos lo ciclos de vida de un StatefulWidget.
ref.read(counterProvider);
}

@override
Widget build(BuildContext context) {
// También podemos usar "ref" para escuchar a un provider dentro del método build
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Extendiendo un HookConsumerWidget en lugar de HookWidget

Esta solución es específica para los usuarios de flutter_hooks. Dado que flutter_hooks requiere extender HookWidget para funcionar, los widgets que usan hooks no pueden extender ConsumerWidget.

Una solución, que es posible gracias al paquete hooks_riverpod, es reemplazar HookWidget con HookConsumerWidget. HookConsumerWidget actúa como ConsumerWidget y HookWidget. Esto permite que un widget escuche a los providers y use hooks.

Un ejemplo sería:

class HomeView extends HookConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget permite usar hooks dentro del método build
final state = useState(0);

// También podemos usar el parámetro ref para escuchar a los providers.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Widgets: Consumer y HookConsumer

Una solución final para obtener una "ref" dentro de los widgets es usar Consumer/HookConsumer.

Estas clases son widgets que se pueden usar para obtener un "ref", con las mismas propiedades que ConsumerWidget/HookConsumerWidget.

Estos widgets pueden ser una forma de obtener una "ref" sin tener que definir una clase.

Un ejemplo sería:

Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// Al igual que HookConsumerWidget, podemos usar hooks dentro del builder.
final state = useState(0);

// También podemos usar el parámetro ref para escuchar a los providers.
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
)

Uso de ref para interactuar con los providers

Ahora que tenemos una "ref", podemos comenzar a usarla.

Hay tres usos principales para "ref":

  • Obteniendo el valor de un provider y escuchando los cambios, de manera que cuando este valor cambie, este reconstruirá el widget o provider que se suscribió al valor. Eso se puede hacer usando ref.watch.
  • Agregando un listener a un provider, para ejecutar una acción cada vez que cambie ese provider. Eso se puede hacer usando ref.listen.
  • Obteniendo el valor de un provider ignorando los cambios. Esto es útil cuando necesitamos el valor de un provider en una función callback de un evento, como en un onPressed callback. Eso se puede hacer usando ref.read.
NOTA

Siempre que sea posible, prefiera usar ref.watch sobre ref.read o ref.listen para implementar una característica. Al cambiar su implementación para que dependa de ref.watch, se vuelve tanto reactivo como declarativo, lo que hace que su aplicación sea más fácil de mantener.

Uso de ref.watch para observar a un provider

Es posible usar ref.watch dentro del método build de un widget o dentro del cuerpo de un provider para que ese widget/provider escuche al provider especificado por la llamada ref.watch:

Por ejemplo, un provider podría usar ref.watch para combinar múltiples providers en un nuevo valor.

Un ejemplo sería filtrar una lista de tareas pendientes. Podríamos tener dos providers:

  • filterTypeProvider, un provider que expone el tipo de filtro actual (ninguno, mostrar solo tareas completadas, ...)
  • todosProvider, un provider que expone la lista completa de tareas

Y al usar ref.watch, podríamos crear un tercer provider que combine ambos providers para crear una lista filtrada de tareas:

final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((ref) {
// obtiene tanto el filtro como la lista de tareas
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);

switch (filter) {
case FilterType.completed:
// retorna la lista completa de tareas
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// retorna la lista sin filtrar de tareas
return todos;
}
});

Con este código, filteredTodoListProvider ahora expone la lista filtrada de tareas.

La lista filtrada también se actualizará automáticamente si el filtro o la lista de tareas cambió. Sin embargo, al mismo tiempo, la lista filtrada no se volverá a calcular si ni el filtro ni la lista de tareas cambiaron.

De manera similar, un widget podría usar ref.watch para mostrar una interfaz de usuario que depende de un provider:

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

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

@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref para escuchar a un provider
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

Este fragmento de código muestra un widget que escucha a un provider que almacena un contador. Y si ese contador cambia, el widget se reconstruirá y la interfaz de usuario se actualizará para mostrar el nuevo valor.

PRECAUCIÓN

El método watch no debe llamarse de forma asíncrona, como dentro de un onPressed de un ElevatedButton. Tampoco se debe utilizar dentro initState u otro ciclo de vida del State.

En esos casos, considere usar ref.read en su lugar.

Uso de ref.listen para reaccionar ante un cambio

De manera similar a ref.watch, es posible usar ref.listen para observar un provider.

La principal diferencia entre ellos es que, en lugar de reconstruir el widget/provider si el provider escuchado cambia, ref.listen llamará a una función personalizada.

Eso puede ser útil para realizar acciones cuando ocurre un determinado cambio, como mostrar un snackbar cuando ocurre un error.

El método ref.listen necesita 2 argumentos posicionales, el primero es el provider y el segundo es el callback que queremos ejecutar cuando cambia el estado. Cuando se llame el callback, se pasarán 2 valores, el valor del Estado anterior y el valor del nuevo Estado.

El método ref.listen se puede utilizar dentro del cuerpo de un provider:

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});

o dentro del método build de un widget:

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

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

@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});

return Container();
}
}
PRECAUCIÓN

El método listen no debe llamarse de forma asíncrona, como dentro de un onPressed de un ElevatedButton. Tampoco se debe utilizar dentro del initState u otro ciclo de vida del State.

Uso de ref.read para obtener el estado de un provider una vez

El método ref.read es una forma de obtener el estado de un provider, sin ningún efecto extra.

Se usa comúnmente dentro de las funciones desencadenadas por las interacciones del usuario. Por ejemplo, podemos usar ref.read para incrementar un contador cuando un usuario hace clic en un botón:

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

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

@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Llama a `increment()` de la clase `Counter`.
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
NOTA

El uso de ref.read debe evitarse tanto como sea posible.

Existe como una solución alternativa para los casos en los que usar watch o listen sería demasiado inconveniente de usar. Si puede, casi siempre es mejor usar watch/listen, especialmente watch.

NO usar ref.read dentro del método build

Es posible que tenga la tentación de utilizar ref.read para optimizar el rendimiento de un widget haciendo lo siguiente:

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

Widget build(BuildContext context, WidgetRef ref) {
// usar "read" para ignorar las actualizaciones de un provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
)
}

Pero esta es una práctica muy mala y puede causar errores que son difíciles de rastrear.

El uso de ref.read esta forma se asocia comúnmente con el pensamiento "El valor expuesto por un provider nunca cambia, por lo que usar 'ref.read' es seguro". El problema con esta suposición es que, si bien es posible que hoy ese provider nunca actualice su valor, no hay garantía de que mañana sea igual.

El software tiende a cambiar mucho, y es probable que en el futuro, un valor que antes nunca cambiaba, tenga que cambiar. Pero si usó ref.read, cuando ese valor comienza a cambiar, tendría que revisar todo su código base para cambiar ref.read en ref.watch, lo cual es propenso a errores y es probable que olvides algunos casos.

Mientras que si usaras ref.watch al principio, no tendrías ningún problema.

Pero quería usar ref.read para reducir la cantidad de veces que mi widget se reconstruye.

Si bien el objetivo es aplaudible, es importante tener en cuenta que puede lograr exactamente el mismo efecto (reduciendo la cantidad de builds) usando ref.watch en su lugar.

Los providers ofrecen varias formas de obtener un valor mientras reducen la cantidad de reconstrucciones, que podría usar en su lugar.

Por ejemplo en lugar de

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

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
)
}

podríamos hacer:

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

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
)
}

Ambos fragmentos de código logran el mismo efecto: nuestro botón no se reconstruirá cuando el contador aumente.

Por otro lado, el segundo enfoque admite casos en los que se reinicia el contador. Por ejemplo, otra parte de la aplicación podría llamar:

ref.refresh(counterProvider);

que recrearía el objeto StateController.

Si usáramos ref.read aquí, nuestro botón seguiría usando la instancia anterior de StateController, que se eliminó y ya no debería usarse. Mientras que usar ref.watch correctamente reconstruyó el botón para usar el nuevo StateController.

Decidir qué leer

Dependiendo del provider que desee escuchar, puede tener varios valores posibles que puede escuchar.

Como ejemplo, considere el siguiente StreamProvider:

final userProvider = StreamProvider<User>(...);

Al leer este userProvider, usted puede:

  • leer sincrónicamente el estado actual escuchando de userProvider:

    Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<User> user = ref.watch(userProvider);

    return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
    );
    }
  • obtener el Stream asociado, escuchando userProvider.stream:

    Widget build(BuildContext context, WidgetRef ref) {
    Stream<User> user = ref.watch(userProvider.stream);
    }
  • obtener un Future que se resuelva con el último valor emitido, escuchando userProvider.future:

    Widget build(BuildContext context, WidgetRef ref) {
    Future<User> user = ref.watch(userProvider.future);
    }

Otros providers pueden ofrecer diferentes valores alternativos. Para más información, consulte la documentación de cada provider consultando la referencia de la API.

Uso de "select" para filtrar reconstrucciones

Una característica final a mencionar relacionada con la lectura de providers es la capacidad de reducir la cantidad de veces que se reconstruye un widget/provider, o con qué frecuencia ref.listen ejecuta una función.

Es importante tener esto en cuenta ya que, de forma predeterminada, escuchar a un provider escucha todo el objeto. Pero en algunos casos, es posible que un widget/provider solo se preocupe por algunas propiedades en lugar del objeto completo.

Por ejemplo, un provider puede exponer un User:

abstract class User {
String get name;
int get age;
}

Pero un widget solo necesita usar la propiedad name del usuario:

Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

Si usamos ingenuamente ref.watch, esto reconstruiría el widget cuando cambie la propiedad age del usuario.

La solución es usar select para decirle explícitamente a Riverpod que solo queremos escuchar algunas propiedades de User.

El código actualizado sería:

Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name))
return Text(name);
}

La forma en que esto funciona al usar select es que estamos especificando una función que devuelve la propiedad que nos interesa.

Luego, cada vez que User cambie, Riverpod llamará a esta función y comparará el resultado anterior y el nuevo. Si son diferentes (como cuando cambió la propiedad name), Riverpod reconstruirá el widget. Pero si son iguales (como cuando solo cambió la propiedad age), Riverpod no reconstruirá el widget.

info

También es posible utilizar select con ref.listen:

ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);

Si lo hace, llamará al listener solo cuando cambie la propiedad name.

tip

No tienes que devolver una propiedad del objeto. Cualquier valor que sobrescriba == funcionará. Por ejemplo podrías hacer:

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));