Flutter Riverpod 2.x: O Guia Definitivo

Tempo de leitura: 13 minutes

Riverpod é uma estrutura reativa de cache e vinculação de dados que nasceu como uma evolução do pacote Provider.

Segundo a documentação oficial:

Riverpod é uma reescrita completa do pacote Provider para fazer melhorias que seriam impossíveis de outra forma.

Muitas pessoas ainda o veem como uma estrutura de “gerenciamento de estado”.

Mas é muito mais do que isso.

Na verdade, o Riverpod 2.x pega emprestado muitos conceitos valiosos do React Query e os traz para o mundo do Flutter.

O Riverpod é muito versátil e você pode usá-lo para:

  • Detectar erros de programação em tempo de compilação em vez de em tempo de execução
  • busque, armazene em cache e atualize facilmente dados de uma fonte remota
  • execute cache reativo e atualize facilmente sua interface do usuário
  • dependem do estado assíncrono ou computado
  • crie, use e combine provedores com o mínimo de código clichê
  • descartar o estado de um provedor quando ele não for mais usado
  • escreva código testável e mantenha sua lógica fora da árvore de widgets

O Riverpod implementa padrões bem definidos para recuperar e armazenar dados em cache, para que você não precise reimplementá-los.

Começar a usar o Riverpod é fácil.

Mas há um pouco de curva de aprendizado se você quiser usá-lo em sua capacidade total, e criei este guia para cobrir todos os conceitos e APIs essenciais.

 

Como este guia está organizado

Para facilitar o acompanhamento, organizei este guia em três partes principais:

  1. Por que usar o Riverpod, como instalá-lo e os principais conceitos
  2. Uma visão geral dos oito tipos diferentes de provedores (e quando usá-los)
  3. Recursos adicionais do Riverpod (modificadores, substituições de provedor, filtragem, suporte de teste, registro, etc.)

Este guia é extenso e atualizado e você pode usá-lo como referência, além da documentação oficial.

Exploraremos as principais APIs e conceitos do Riverpod usando exemplos simples.

Um novo pacote riverpod_generator foi publicado como parte da versão Riverpod 2.x. Isso apresenta uma nova API de anotação @riverpod que você pode usar para gerar provedores automaticamente para classes e métodos em seu código (usando geração de código)

Preparar? Vamos começar! 🚀

Por que usar o Riverpod?

Para entender por que precisamos do Riverpod, vamos ver a principal desvantagem do pacote Provider.

Por design, Provider é uma melhoria em relação a InheritedWidget e, como tal, depende da árvore de widgets.

Esta é uma decisão de design infeliz que pode levar ao comum ProviderNotFoundException:

Por outro lado, o Riverpod é seguro para compilação, pois todos os provedores são declarados globalmente e podem ser acessados em qualquer lugar.

Isso significa que você pode criar provedores para manter o estado do aplicativo e a lógica de negócios fora da árvore de widgets.

E como o Riverpod é uma estrutura reativa, fica mais fácil apenas reconstruir seus provedores e widgets quando necessário.

Então, vamos ver como instalá-lo e usá-lo. 👇

 

Instalação do Riverpod

A primeira etapa é adicionar a versão mais recente do flutter_riverpod como uma dependência ao nosso arquivo pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter  
  flutter_riverpod: ^2.3.6

Observação: Se seu aplicativo já usa flutter_hooks, você pode instalar o pacote hooks_riverpod. Isso inclui alguns recursos extras que facilitam a integração do Hooks com o Riverpod. Neste tutorial, vamos nos concentrar em flutter_riverpod apenas para simplificar.

Se você quiser usar o novo Riverpod Generator, precisará instalar alguns pacotes adicionais.

Dica: para adicionar mais facilmente provedores Riverpod em seu código, instale a extensão Flutter Riverpod Snippets para VSCode ou Android Studio/IntelliJ.

Para obter mais informações, leia a página Introdução em Riverpod.dev.

 

ProviderScope

Depois que o Riverpod estiver instalado, podemos agrupar nosso widget root com um ProviderScope:

void main() {
  // envolva o aplicativo inteiro com um ProviderScope 
  // para que os widgets possam ler os provedores
  runApp(ProviderScope(
    child: MyApp(),
  ));
}

ProviderScope é um widget que armazena o estado de todos os provedores que criamos.

Nos bastidores, ProviderScope cria uma instância de ProviderContainer. Na maioria das vezes, você não precisará se preocupar com o ProviderContainer ou usá-lo diretamente. Para obter mais detalhes sobre ProviderContainer e UncontrolProviderScope

Após concluir a configuração inicial, podemos começar a aprender sobre os provedores.

 

O que é um Provedor Riverpod?

A documentação do Riverpod define um provedor como um objeto que encapsula um pedaço de estado e permite ouvir esse estado.

Com Riverpod, os provedores são o centro de tudo:

  • Eles substituem completamente os padrões de design, como singletons, localizadores de serviço, injeção de dependência e InheritedWidgets.
  • Eles permitem armazenar algum estado e acessá-lo facilmente em vários locais.
  • Eles permitem que você otimize o desempenho filtrando reconstruções de widgets ou armazenando em cache computações de estado dispendiosas.
  • Eles tornam seu código mais testável, pois cada provedor pode ser substituído para se comportar de maneira diferente durante um teste.

Então, vamos ver como usá-los. 👇

 

Criando e lendo um provedor

Vamos começar criando um provedor “Hello world” básico:

// provedor que retorna um valor de string
final helloWorldProvider = Provider<String>((ref) {
  return 'Hello world';
});

Isso é feito de três coisas:

  1. A declaração: final helloWorldProvider é a variável global que usaremos para ler o estado do provedor.
  2. O provedor: Provider<String> nos diz que tipo de provedor estamos usando (mais sobre isso abaixo) e o tipo de estado que ele contém.
  3. Uma função que cria o estado. Isso nos dá um parâmetro ref que podemos usar para ler outros provedores, executar alguma lógica de descarte personalizada e muito mais.

Uma vez que temos um provedor, como o usamos dentro de um widget?

class HelloWorldWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /* how to read the provider value here? */,
    );
  }
}

Todos os widgets Flutter têm um objeto BuildContext que podemos usar para acessar coisas dentro da árvore de widgets (como Theme.of(context)).

Mas os provedores Riverpod vivem fora da árvore de widgets e, para lê-los, precisamos de um objeto ref adicional. Aqui estão três maneiras diferentes de obtê-lo. 👇

 

1. Usando um ConsumerWidget

A maneira mais simples é usar um ConsumerWidget:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

// 1. widget class now extends [ConsumerWidget]
class HelloWorldWidget extends ConsumerWidget {
  @override
  // 2. build method has an extra [WidgetRef] argument
  Widget build(BuildContext context, WidgetRef ref) {
    // 3. use ref.watch() to get the value of the provider
    final helloWorld = ref.watch(helloWorldProvider);
    return Text(helloWorld);
  }
}

Subclassificando ConsumerWidget em vez de StatelessWidget, o método build de nosso widget obtém um objeto ref extra (do tipo WidgetRef) que podemos usar para observar nosso provedor.

Usar o ConsumerWidget é a opção mais comum e a que você deve escolher na maioria das vezes.

 

2. Usando um Consumer

Como alternativa, podemos agrupar nosso text widget  com um Consumer:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

class HelloWorldWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 1. Add a Consumer
    return Consumer(
      // 2. specify the builder and obtain a WidgetRef
      builder: (_, WidgetRef ref, __) {
        // 3. use ref.watch() to get the value of the provider
        final helloWorld = ref.watch(helloWorldProvider);
        return Text(helloWorld);
      },
    );
  }
}

Nesse caso, o objeto “ref” é um dos argumentos do construtor do Consumer, e podemos usá-lo para observar o valor do provedor.

Isso funciona, mas é mais detalhado do que a solução anterior.

Então, quando devemos usar um Consumer em vez de um ConsumerWidget?

Aqui está um exemplo:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

class HelloWorldWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      // 1. Add a Consumer
      body: Consumer(
        // 2. specify the builder and obtain a WidgetRef
        builder: (_, WidgetRef ref, __) {
          // 3. use ref.watch() to get the value of the provider
          final helloWorld = ref.watch(helloWorldProvider);
          return Text(helloWorld);
        },
      ),
    );
  }
}

Nesse caso, estamos agrupando apenas o Text com um widget Consumer, mas não o Scaffold pai:

Scaffold
├─ AppBar
└─ Consumer
    └─ Text

Como resultado, apenas o Text será reconstruído se o valor do xprovider for alterado (mais sobre isso abaixo).

Isso pode parecer um pequeno detalhe, mas se você tiver uma grande classe de widget com um layout complexo, poderá usar o Consumer para reconstruir apenas os widgets que dependem do provedor.

A criação de widgets pequenos e reutilizáveis favorece a composição, levando a um código conciso, com melhor desempenho e mais fácil de raciocinar.

Se você seguir esse princípio e criar widgets pequenos e reutilizáveis, naturalmente usará o ConsumerWidget na maioria das vezes.

 

3. Usando ConsumerStatefulWidget e ConsumerState

ConsumerWidget é um bom substituto para StatelessWidget e nos dá uma maneira conveniente de acessar provedores com o mínimo de código.

Mas e se tivermos um StatefulWidget?

Aqui está o mesmo exemplo de hello world:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

// 1. extend [ConsumerStatefulWidget]
class HelloWorldWidget extends ConsumerStatefulWidget {
  @override
  ConsumerState<HelloWorldWidget> createState() => _HelloWorldWidgetState();
}

// 2. extend [ConsumerState]
class _HelloWorldWidgetState extends ConsumerState<HelloWorldWidget> {
  @override
  void initState() {
    super.initState();
    // 3. if needed, we can read the provider inside initState
    final helloWorld = ref.read(helloWorldProvider);
    print(helloWorld); // "Hello world"
  }

  @override
  Widget build(BuildContext context) {
    // 4. use ref.watch() to get the value of the provider
    final helloWorld = ref.watch(helloWorldProvider);
    return Text(helloWorld);
  }
}

Criando uma subclasse de ConsumerStatefulWidget e ConsumerState, podemos chamar ref.watch() no método build exatamente como fizemos antes.

E se precisarmos ler o valor do provedor em qualquer um dos outros métodos de ciclo de vida do widget, podemos usar ref.read().

Quando criamos uma subclasse de ConsumerState, podemos acessar o objeto ref dentro de todos os métodos de ciclo de vida do widget. Isso ocorre porque ConsumerState declara WidgetRef como uma propriedade, assim como a classe Flutter State declara BuildContext como uma propriedade que pode ser acessada diretamente dentro de todos os métodos de ciclo de vida do widget.

Se você usar o pacote hooks_riverpod, também poderá usar HookConsumerWidget e StatefulHookConsumerWidget. A documentação oficial cobre esses widgets com mais detalhes.

 

O que é um WidgetRef?

Como vimos, podemos observar o valor de um provedor usando um objeto ref do tipo WidgetRef. Isso está disponível como um argumento quando usamos Consumer ou ConsumerWidget e como uma propriedade quando criamos uma subclasse de ConsumerState.

A documentação do Riverpod define WidgetRef como um objeto que permite que widgets interajam com provedores.

Observe que existem algumas semelhanças entre BuildContext e WidgetRef:

  • BuildContext nos permite acessar widgets ancestrais na árvore de widgets (como Theme.of(context) e MediaQuery.of(context))
  • WidgetRef nos permite acessar qualquer provedor dentro de nosso app

Em outras palavras, WidgetRef nos permite acessar qualquer provedor em nossa base de código (desde que importemos o arquivo correspondente). Isso ocorre por design porque todos os provedores Riverpod são globais.

Isso é significativo porque manter o estado e a lógica do aplicativo dentro de nossos widgets leva a uma separação insatisfatória de preocupações. Movê-lo para dentro de nossos provedores torna nosso código mais testável e sustentável. 👍

 

Oito tipos diferentes de provedores

Até agora, aprendemos como criar um Provider simples e assisti-lo dentro de um widget usando um objeto ref.

Mas o Riverpod oferece oito tipos diferentes de provedores, todos adequados para casos de uso separados:

  1. Provider
  2. StateProvider (legado)
  3. StateNotifierProvider (legado)
  4. FutureProvider
  5. StreamProvider
  6. ChangeNotifierProvider (legado)
  7. NotifierProvider (novo no Riverpod 2.0) (Próximo Artigo)
  8. AsyncNotifierProvider (novo no Riverpod 2.0) (Próximo Artigo)

Então, vamos analisá-los e entender quando usá-los.

Se você usar o novo pacote riverpod_generator, não precisará mais declarar seus provedores manualmente (embora eu ainda recomende que você se familiarize com todos os oito tipos de provedores).

 

1. Provider

Já aprendemos sobre este:

// provider that returns a string value
final helloWorldProvider = Provider<String>((ref) {
  return 'Hello world';
});

Provider é ótimo para acessar dependências e objetos que não mudam.

Você pode usar isso para acessar um repositório, um registrador ou alguma outra classe que não contenha estado mutável.

Por exemplo, aqui está um provedor que retorna um DateFormat:

// declare the provider
final dateFormatterProvider = Provider<DateFormat>((ref) {
  return DateFormat.MMMEd();
});

class SomeWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // retrieve the formatter
    final formatter = ref.watch(dateFormatterProvider);
    // use it
    return Text(formatter.format(DateTime.now()));
  }
}

Provider é ótimo para acessar dependências que não mudam, como os repositórios em nosso aplicativo.

Mais informações aqui:

 

2. StateProvider

StateProvider é ótimo para armazenar objetos de estado simples que podem mudar, como um valor de contador:

final counterStateProvider = StateProvider<int>((ref) {
  return 0;
});

Se você observar dentro do método build, o widget será reconstruído quando o estado mudar.

E você pode atualizar seu estado dentro de um callback de botão chamando ref.read():

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1. watch the provider and rebuild when the value changes
    final counter = ref.watch(counterStateProvider);
    return ElevatedButton(
      // 2. use the value
      child: Text('Value: $counter'),
      // 3. change the state inside a button callback
      onPressed: () => ref.read(counterStateProvider.notifier).state++,
    );
  }
}

StateProvider é ideal para armazenar variáveis de estado simples, como enums, strings, booleanos e números. Notifier também pode ser usado para a mesma finalidade e é mais flexível. Para um estado mais complexo ou assíncrono, use AsyncNotifierProvider, FutureProvider ou StreamProvider conforme descrito abaixo.

Mais informações e exemplos aqui:

 

3. StateNotifierProvider

Use isso para ouvir e expor um StateNotifier.

StateNotifierProvider e StateNotifier são ideais para gerenciar o estado que pode mudar em reação a um evento ou interação do usuário.

Por exemplo, aqui está uma classe Clock simples:

import 'dart:async';

class Clock extends StateNotifier<DateTime> {
  // 1. initialize with current time
  Clock() : super(DateTime.now()) {
    // 2. create a timer that fires every second
    _timer = Timer.periodic(Duration(seconds: 1), (_) {
      // 3. update the state with the current time
      state = DateTime.now();
    });
  }

  late final Timer _timer;

  // 4. cancel the timer when finished
  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }
}

Essa classe define o estado inicial chamando super(DateTime.now()) no construtor e o atualiza a cada segundo usando um cronômetro periódico.

Assim que tivermos isso, podemos criar um novo provider:

// Note: StateNotifierProvider has *two* type annotations
final clockProvider = StateNotifierProvider<Clock, DateTime>((ref) {
  return Clock();
});

Em seguida, podemos observar o clockProvider dentro de um ConsumerWidget para obter a hora atual e mostrá-la dentro de um text widget:

import 'package:intl/intl.dart';

class ClockWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // watch the StateNotifierProvider to return a DateTime (the state)
    final currentTime = ref.watch(clockProvider);
    // format the time as `hh:mm:ss`
    final timeFormatted = DateFormat.Hms().format(currentTime);
    return Text(timeFormatted);
  }
}

Como estamos usando ref.watch(clockProvider), nosso widget será reconstruído toda vez que o estado mudar (uma vez a cada segundo) e mostrará a hora atualizada.

Observação: ref.watch(clockProvider) retorna o estado do provedor. Para acessar o objeto notificador de estado subjacente, chame ref.read(clockProvider.notifier).

Mais informações aqui:

A partir do Riverpod 2.0, StateNotifier é considerado legado e pode ser substituído pela nova classe AsyncNotifier.

Observe que usar StateNotifierProvider é um exagero se você precisar apenas ler alguns dados assíncronos. É para isso que serve o FutureProvider. 👇

 

4. FutureProvider

Quer obter o resultado de uma chamada de API que retorna um Future?

Depois é só criar um FutureProvider assim:

final weatherFutureProvider = FutureProvider.autoDispose<Weather>((ref) {
  // get repository from the provider below
  final weatherRepository = ref.watch(weatherRepositoryProvider);
  // call method that returns a Future<Weather>
  return weatherRepository.getWeather(city: 'London');
});

// example weather repository provider
final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
  return WeatherRepository(); // declared elsewhere
});

FutureProvider é frequentemente usado com o modificador autoDispose.

Em seguida, você pode observá-lo no método build e usar a correspondência de padrões para mapear o resultanteAsyncValue (data, loading, error) para sua interface do usuário:

Widget build(BuildContext context, WidgetRef ref) {
  // watch the FutureProvider and get an AsyncValue<Weather>
  final weatherAsync = ref.watch(weatherFutureProvider);
  // use pattern matching to map the state to the UI
  return weatherAsync.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (weather) => Text(weather.toString()),
  );
}

Observação: quando você assiste a um FutureProvider<T> ou StreamProvider<T>, o tipo de retorno é um AsyncValue<T>. AsyncValue é uma classe utilitária para lidar com dados assíncronos no Riverpod.

O FutureProvider é muito poderoso e você pode usá-lo para:

  • executar e armazenar em cache operações assíncronas (como solicitações de rede)
  • lidar com os estados de erro e carregamento de operações assíncronas
  • combinar vários valores assíncronos em outro valor
  • rebuscar e atualizar dados (útil para operações pull-to-refresh)

Mais informações aqui:

 

5. StreamProvider

Use StreamProvider para assistir a um Stream de resultados de uma API em tempo real e reconstruir a interface do usuário de forma reativa.

Por exemplo, veja como criar um StreamProvider para o método authStateChanges da classe FirebaseAuth:

final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
  // get FirebaseAuth from the provider below
  final firebaseAuth = ref.watch(firebaseAuthProvider);
  // call a method that returns a Stream<User?>
  return firebaseAuth.authStateChanges();
});

// provider to access the FirebaseAuth instance
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
  return FirebaseAuth.instance;
});

E aqui está como usá-lo dentro de um widget:

Widget build(BuildContext context, WidgetRef ref) {
  // watch the StreamProvider and get an AsyncValue<User?>
  final authStateAsync = ref.watch(authStateChangesProvider);
  // use pattern matching to map the state to the UI
  return authStateAsync.when(
    data: (user) => user != null ? HomePage() : SignInPage(),
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
  );
}

StreamProvider tem muitos benefícios sobre o widget StreamBuilder, todos listados aqui:

 

6. ChangeNotifierProvider

A classe ChangeNotifier faz parte do Flutter SDK.

Podemos usá-lo para armazenar algum estado e notificar os ouvintes quando ele mudar.

Por exemplo, aqui está uma subclasse ChangeNotifier junto com o ChangeNotifierProvider correspondente:

class AuthController extends ChangeNotifier {
  // mutable state
  User? user;
  // computed state
  bool get isSignedIn => user != null;

  Future<void> signOut() {
    // update state
    user = null;
    // and notify any listeners
    notifyListeners();
  }
}

final authControllerProvider = ChangeNotifierProvider<AuthController>((ref) {
  return AuthController();
});

E aqui está o método de construção do widget mostrando como usá-lo:

Widget build(BuildContext context, WidgetRef ref) {
  return ElevatedButton(
    onPressed: () => ref.read(authControllerProvider).signOut(),
    child: const Text('Logout'),
  );
}

A API ChangeNotifier facilita a quebra de duas regras importantes: estado imutável e fluxo de dados unidirecional.

Como resultado, ChangeNotifier é desencorajado e devemos usar StateNotifier em seu lugar.

Quando usado incorretamente, ChangeNotifier leva ao estado mutável e torna nosso código mais difícil de manter. StateNotifier nos fornece uma API simples para lidar com estado imutável.

Mais informações aqui:

 

Novo no Riverpod 2.0: NotifierProvider e AsyncNotifierProvider

Riverpod 2.0 introduziu novas classes Notifier e AsyncNotifier, juntamente com seus provedores correspondentes.

Quando usar ref.watch vs ref.read?

Nos exemplos acima, encontramos duas maneiras de ler providers: ref.read e ref.watch.

Para obter o valor de um provedor dentro de um método build, sempre usamos ref.watch. Isso garante que, se o valor do provedor for alterado, reconstruiremos os widgets que dependem dele.

Mas há casos em que não devemos usar ref.watch.

Por exemplo, dentro do retorno de chamada onPressed de um botão, devemos usar ref.read:

final counterStateProvider = StateProvider<int>((_) => 0);

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1. watch the provider and rebuild when the value changes
    final counter = ref.watch(counterStateProvider);
    return ElevatedButton(
      // 2. use the value
      child: Text('Value: $counter'),
      // 3. change the state inside a button callback
      onPressed: () => ref.read(counterStateProvider.notifier).state++,
    );
  }
}

Como regra geral, devemos:

  • chame ref.watch(provider) para observar o estado de um provedor no método build e reconstruir um widget quando ele mudar
  • chame ref.read(provider) para ler o estado de um provedor apenas uma vez (isso pode ser útil em initState ou outros métodos de ciclo de vida)

No entanto, no código acima, chamamos ref.read(provider.notifier) e o usamos para modificar seu estado.

A sintaxe .notifier está disponível apenas com StateProvider e StateNotifierProvider e funciona da seguinte maneira:

  • chame ref.read(provider.notifier) em um StateProvider<T> para retornar o StateController<T> subjacente que podemos usar para modificar o estado
  • chame ref.read(provider.notifier) em um StateNotifierProvider<T> para retornar o StateNotifier<T> subjacente para que possamos chamar métodos nele

Além de usar ref.watch e ref.read dentro de nossos widgets, também podemos usá-los dentro de nossos provedores.

Juntamente com ref.read e ref.watch, também temos ref.listen. 👇

 

Listening to Provider State Changes

Às vezes queremos mostrar um alert dialog ou um SnackBar quando o estado de um provedor muda.

Podemos fazer isso chamando ref.listen() dentro do método build:

final counterStateProvider = StateProvider<int>((_) => 0);

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // if we use a StateProvider<T>, the type of the previous and current 
    // values is StateController<T>
    ref.listen<StateController<int>>(counterStateProvider.state, (previous, current) {
      // note: this callback executes when the provider value changes,
      // not when the build method is called
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Value is ${current.state}')),
      );
    });
    // watch the provider and rebuild when the value changes
    final counter = ref.watch(counterStateProvider);
    return ElevatedButton(
      // use the value
      child: Text('Value: $counter'),
      // change the state inside a button callback
      onPressed: () => ref.read(counterStateProvider.notifier).state++,
    );
  }
}

Nesse caso, o callback nos dá o estado anterior e atual do provedor, e podemos usá-lo para mostrar um SnackBar.

ref.listen() nos dá um retorno de chamada que é executado quando o valor do provedor muda, não quando o método build é chamado. Portanto, podemos usá-lo para executar qualquer código assíncrono (como mostrar uma caixa de diálogo), assim como fazemos dentro de callbacks de botão.

Além de watch, read e listen, o Riverpod 2.0 introduziu novos métodos que podemos usar para atualizar ou invalidar explicitamente um provedor. Vou cobri-los em um artigo separado.