Flutter Riverpod 2.x: O Guia Definitivo
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.
Conteudo
Como este guia está organizado
Para facilitar o acompanhamento, organizei este guia em três partes principais:
- Por que usar o Riverpod, como instalá-lo e os principais conceitos
- Uma visão geral dos oito tipos diferentes de provedores (e quando usá-los)
- 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 deProviderContainer
. Na maioria das vezes, você não precisará se preocupar com oProviderContainer
ou usá-lo diretamente. Para obter mais detalhes sobreProviderContainer
eUncontrolProviderScope
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
eInheritedWidgets
. - 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:
- A declaração:
final helloWorldProvider
é a variável global que usaremos para ler o estado do provedor. - 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. - 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 objetoref
dentro de todos os métodos de ciclo de vida do widget. Isso ocorre porqueConsumerState
declaraWidgetRef
como uma propriedade, assim como a classe FlutterState
declaraBuildContext
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á usarHookConsumerWidget
eStatefulHookConsumerWidget
. 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 (comoTheme.of(context)
eMediaQuery.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:
Provider
StateProvider
(legado)StateNotifierProvider
(legado)FutureProvider
StreamProvider
ChangeNotifierProvider
(legado)NotifierProvider
(novo no Riverpod 2.0) (Próximo Artigo)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, chameref.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 modificadorautoDispose
.
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étodobuild
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 umStateProvider<T>
para retornar o StateController<T> subjacente que podemos usar para modificar o estado - chame
ref.read(provider.notifier)
em umStateNotifierProvider<T>
para retornar oStateNotifier<T>
subjacente para que possamos chamar métodos nele
Além de usar
ref.watch
eref.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
elisten
, 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.