Quando desenvolvemos aplicativos Flutter, frequentemente precisamos lidar com informações que mudam ao longo do tempo: o número exibido em um contador, se o usuário está logado ou não, os itens em um carrinho de compras, o tema atual do app. Essas informações que podem mudar e precisam ser compartilhadas entre diferentes partes do aplicativo são o que chamamos de estado. Gerenciar estado significa controlar essas informações de forma organizada: onde elas ficam armazenadas, como são modificadas e como os widgets são notificados quando elas mudam para atualizar a interface.
Gerenciar o estado de um aplicativo Flutter pode parecer complicado no início, mas com o Riverpod, essa tarefa se torna muito mais simples e poderosa. Riverpod é uma evolução do Provider, criado pelo mesmo desenvolvedor (Remi Rousselet), trazendo melhorias significativas como não depender do BuildContext e melhor testabilidade.
Neste guia, vamos construir um aplicativo simples de contador para demonstrar como o Riverpod funciona na prática. Você aprenderá a criar providers, consumir seus valores e reagir a mudanças de estado de forma limpa e eficiente.
Versões utilizadas neste tutorial:
- Flutter: 3.38.3
- flutter_riverpod: 3.0.3
Por que usar Riverpod?
- Sem Context: Você pode acessar providers de qualquer lugar, sem precisar do
BuildContextcomo era necessário no Provider. - Segurança em Tempo de Compilação: Erros de tipo são detectados durante a compilação, não em tempo de execução.
- Testável: Muito mais fácil de testar do que outras soluções de gerenciamento de estado.
- Menos BoilerPlate: Código mais limpo e direto que outras soluções como BLoC (Business Logic Component), outro padrão de gerenciamento de estado muito popular no Flutter. É ótimo para apps grandes e complexos, mas pode ser “demais” para projetos simples.
- Evolução do Provider: Criado pelo mesmo desenvolvedor, corrigindo problemas de design do Provider original.
Passo 1: Criando o Projeto e Adicionando Dependências
Vamos começar criando um novo projeto Flutter e adicionando o Riverpod.
Crie o projeto:
flutter create riverpod_exampleNavegue para a pasta do projeto:
cd riverpod_exampleAdicione o Riverpod: Usaremos o
flutter_riverpod, que é a versão específica para Flutter.flutter pub add flutter_riverpod
Passo 2: Entendendo os Conceitos Básicos
Antes de mergulhar no código, vamos entender os principais conceitos do Riverpod de forma bem clara:
Notifier: É uma classe especial que funciona como uma “estação de rádio”. Quando o valor muda, ela transmite atualizações para todos os widgets que estão “sintonizados” nela. Pense nela como uma estação de rádio que transmite atualizações para todos os aparelhos sintonizados.
NotifierProvider: É o widget que disponibiliza o
Notifierpara toda a aplicação. É como instalar a antena da estação de rádio - sem ela, ninguém consegue sintonizar o sinal. Geralmente declaramos ele em um arquivo separado de providers.ProviderScope: Um widget que deve envolver toda a sua aplicação no
main.dart. Ele é o “gerente geral” que cuida de todos os providers. Sem ele, nada funciona - é como a torre de transmissão principal que coordena todas as antenas.ConsumerWidget: Um widget especial que “escuta” as transmissões do provider. Toda vez que o
Notifierenvia uma atualização, oConsumerWidgetreconstrói automaticamente. É como um rádio que automaticamente atualiza a informação na tela quando recebe um novo sinal. Importante: É dentro doConsumerWidgetque temos acesso aoref, que nos permite usarref.watcheref.read. Se você usar umStatelessWidgetnormal, não terá acesso aoref.ref.watch(): Um método que sintoniza na “frequência” do provider e fica ouvindo as atualizações. Quando algo muda, o widget é reconstruído automaticamente. Use dentro do método
build()quando você quer que a tela atualize sozinha. Só funciona dentro de umConsumerWidget!ref.read(): Este método apenas “dá uma olhadinha” no valor atual sem ficar ouvindo a transmissão. É como verificar rapidamente qual música está tocando sem ligar o rádio. Use dentro de callbacks (como
onPressedde um botão ouonChangedde um TextField) quando você só quer executar uma ação pontual. Também só funciona dentro de umConsumerWidget!
Passo 3: Estrutura do Projeto
Vamos criar uma estrutura simples e organizada. Nosso app terá duas páginas para demonstrar como o Riverpod compartilha estado entre telas diferentes:
lib/
├── main.dart
├── providers/
│ └── counter_provider.dart
└── pages/
├── counter_page.dart # Página com os botões
└── display_page.dart # Página que só exibe o valor
Passo 4: Criando o Provider do Contador
lib/providers/counter_provider.dart
Vamos criar um Notifier bem simples. A partir do Riverpod 3.0+, não podemos mais acessar o state diretamente de fora do Notifier, então criamos um método setValue:
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Classe que gerencia o estado do contador
class CounterNotifier extends Notifier<int> {
// Retorna o valor inicial
@override
int build() => 0;
// Método para modificar o valor
void setValue(int value) {
state = value;
}
}
// Provider que disponibiliza o CounterNotifier
final counterProvider = NotifierProvider<CounterNotifier, int>(() {
return CounterNotifier();
});
Como funciona:
- Para ler o valor:
ref.watch(counterProvider)- lê e fica observando mudanças (reconstrói o widget quando muda) - Para modificar o valor:
ref.read(counterProvider.notifier).setValue(novoValor)- acessa o notifier e chama o método para mudar o estado
Por que .notifier? Quando fazemos ref.read(counterProvider), pegamos apenas o valor (o int, o número). Mas o valor sozinho não tem métodos. Para modificar, precisamos acessar o gerenciador (o CounterNotifier), que é quem tem os métodos como setValue(). Por isso usamos ref.read(counterProvider.notifier) - estamos pegando o gerenciador, não o valor.
O Riverpod cuida de avisar todos os widgets que estão usando ref.watch automaticamente quando você chama setValue.
Passo 5: Criando as Páginas
Agora vamos criar duas páginas para demonstrar o poder do Riverpod: uma página com um campo de texto para digitar o valor do contador, e outra que apenas exibe o valor. O interessante é que não vamos passar o valor como parâmetro entre as páginas - o Riverpod cuida disso!
lib/pages/counter_page.dart
Esta é a página principal com o campo de texto para modificar o contador:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/counter_provider.dart';
import 'display_page.dart';
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watch observa o provider e reconstrói quando muda
final counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Digite o Valor'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Valor atual:',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 16),
Text(
'$counter',
style: const TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 32),
TextField(
decoration: const InputDecoration(
labelText: 'Digite um número',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.edit),
),
keyboardType: TextInputType.number,
onChanged: (value) {
// Tenta converter o texto para número
final numero = int.tryParse(value);
if (numero != null) {
// ref.read não reconstrói, apenas modifica o estado
ref.read(counterProvider.notifier).setValue(numero);
}
},
),
const SizedBox(height: 48),
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DisplayPage()),
);
},
icon: const Icon(Icons.visibility),
label: const Text('Ver em Outra Página'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
),
),
],
),
),
),
);
}
}
lib/pages/display_page.dart
Esta página apenas exibe o valor do contador. Note que não recebemos nenhum parâmetro - o Riverpod fornece o valor automaticamente. Aqui vamos usar ref.read ao invés de ref.watch para demonstrar a diferença:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/counter_provider.dart';
class DisplayPage extends ConsumerWidget {
const DisplayPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Aqui usamos ref.read - pega o valor UMA VEZ quando a página é criada
final counter = ref.read(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Visualização'),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.remove_red_eye,
size: 64,
color: Colors.green,
),
const SizedBox(height: 24),
const Text(
'O contador está em:',
style: TextStyle(fontSize: 24),
),
const SizedBox(height: 16),
Text(
'$counter',
style: const TextStyle(
fontSize: 96,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
const SizedBox(height: 48),
Card(
margin: const EdgeInsets.symmetric(horizontal: 32),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: const [
Icon(Icons.info_outline, size: 48, color: Colors.green),
SizedBox(height: 16),
Text(
'Esta página usa ref.read',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
SizedBox(height: 8),
Text(
'Ela pega o valor atual quando é criada. Volte, mude o contador, e volte aqui - verá o novo valor!',
textAlign: TextAlign.center,
),
],
),
),
),
],
),
),
);
}
}
ref.watch vs ref.read na prática:
CounterPage (primeira) usa
ref.watch: O número atualiza automaticamente enquanto você digita no TextField porque está “observando” as mudanças.DisplayPage (segunda) usa
ref.read: Pega o valor apenas quando a página é criada. Como o Flutter recria a página toda vez que você navega pra ela, sempre verá o valor mais atual. Porém, se algo mudasse o contador enquanto você está olhando essa página, ela não atualizaria sozinha.
Quando usar cada um:
- Use
ref.watchquando precisar que a tela atualize automaticamente ao detectar mudanças - Use
ref.readquando quiser pegar o valor uma vez só, sem ficar observando (economiza recursos)
Passo 6: Configurando o Main
lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'pages/counter_page.dart';
void main() {
// ProviderScope deve envolver todo o app
// Ele é o "gerente geral" de todos os providers
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const CounterPage(),
);
}
}
Passo 7: Entendendo o Fluxo de Dados
Criação do Provider: Criamos uma classe
CounterNotifierque estendeNotifier<int>e define o valor inicial no métodobuild(). Também criamos um métodosetValue()para modificar o estado.ProviderScope: Envolvemos o app com
ProviderScopenomain.dartpara que todos os providers estejam disponíveis em toda a aplicação.Compartilhamento entre Páginas: Ambas as páginas (
CounterPageeDisplayPage) acessam o mesmo provider através doref. Não precisamos passar parâmetros noNavigator.push()- o Riverpod gerencia isso!Leitura com
ref.watcheref.read:- A
CounterPageusaref.watch(counterProvider)para observar mudanças - por isso o número atualiza automaticamente enquanto você digita no TextField - A
DisplayPageusaref.read(counterProvider)para pegar o valor apenas quando a página é criada - como o Flutter recria a página toda vez que você navega pra ela, sempre mostra o valor atual
- A
Modificação com
ref.read: Para modificar o estado, usamosref.read(counterProvider.notifier).setValue(novoValor). Simples e direto!
ref.watch vs ref.read
É importante entender as diferentes formas de acessar o provider:
ref.watch(counterProvider): Lê o valor E se inscreve para mudanças. O widget será reconstruído automaticamente quando o estado mudar. Use no métodobuild()(o método que constrói/desenha seu widget na tela) quando precisar que a tela atualize sozinha ao detectar mudanças. Quando você usaref.watchdentro dobuild()de um widget, o Riverpod “registra” que aquele widget quer ser avisado de mudanças - então toda vez que o estado muda, o Riverpod chama o métodobuild()de novo, redesenhando o widget com o novo valor.ref.read(counterProvider): Lê o valor uma única vez, SEM se inscrever para mudanças. O widget não será reconstruído se o estado mudar. Use quando quiser apenas pegar o valor atual sem ficar observando (economiza recursos). No nosso exemplo, usamos isso naDisplayPage.ref.read(counterProvider.notifier).setValue(): Acessa o notifier (o gerenciador) para chamar métodos que modificam o estado. Use dentro de callbacks (comoonPressedde um botão ouonChangedde um TextField) quando você quer apenas executar uma ação de modificação, sem reconstruir o widget. Nunca useref.watchdentro de callbacks - isso causaria problemas ao tentar reconstruir o widget no meio de uma ação.
Exemplo Prático
@override
Widget build(BuildContext context, WidgetRef ref) { // <-- Método build do widget
// watch: reconstrói quando counter muda (observa mudanças)
// Quando o estado muda, este método build() roda de novo
final counter = ref.watch(counterProvider);
// read: pega o valor uma vez, não observa mudanças
// final counter = ref.read(counterProvider);
return Column(
children: [
Text('$counter'), // Atualiza automaticamente se usou watch
TextField(
onChanged: (value) { // <-- Callback, não é o build()
final numero = int.tryParse(value);
if (numero != null) {
// read + notifier: acessa o gerenciador para modificar
// Aqui usamos read porque estamos dentro de um callback
ref.read(counterProvider.notifier).setValue(numero);
}
},
),
],
);
}
Trabalhando com Múltiplos Valores
Uma dúvida comum: e se eu precisar armazenar mais de um valor no meu provider? Por exemplo, três inteiros ao invés de um? Com Riverpod, você pode criar uma classe para agrupar valores relacionados:
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Classe para agrupar valores relacionados
class CountersState {
final int counter1;
final int counter2;
final int counter3;
CountersState({
required this.counter1,
required this.counter2,
required this.counter3,
});
}
// Notifier que gerencia o estado composto
class CountersNotifier extends Notifier<CountersState> {
@override
CountersState build() {
return CountersState(counter1: 0, counter2: 0, counter3: 0);
}
// Métodos para modificar valores individuais
void setCounter1(int value) {
state = CountersState(
counter1: value,
counter2: state.counter2,
counter3: state.counter3,
);
}
void setCounter2(int value) {
state = CountersState(
counter1: state.counter1,
counter2: value,
counter3: state.counter3,
);
}
void setCounter3(int value) {
state = CountersState(
counter1: state.counter1,
counter2: state.counter2,
counter3: value,
);
}
}
// Provider
final countersProvider = NotifierProvider<CountersNotifier, CountersState>(() {
return CountersNotifier();
});
Como usar:
// Ler valores
final state = ref.watch(countersProvider);
Text('Counter 1: ${state.counter1}');
// Modificar valores
ref.read(countersProvider.notifier).setCounter1(10);
Dica: Para facilitar a modificação de apenas um valor, você pode adicionar um método copyWith na classe CountersState, como mostramos na seção de boas práticas a seguir.
Boas Práticas com Riverpod
Mantenha os Providers Organizados: Crie uma pasta
providerse separe por funcionalidade.Evite Lógica no Build: Se precisar de lógica complexa, crie métodos no
Notifierao invés de manipular o estado diretamente.Use
ConsumerWidgetapenas quando necessário: Se um widget não precisa acessar providers, pode ser umStatelessWidgetnormal.Use
autoDisposequando apropriado: Por padrão, os providers ficam “vivos” na memória o tempo todo, mesmo que nenhum widget esteja usando. OautoDisposefaz com que o provider seja automaticamente destruído quando o último widget que estava usando ele for removido da tela. Isso economiza memória!// SEM autoDispose: fica na memória para sempre final counterProvider = NotifierProvider<CounterNotifier, int>(() { return CounterNotifier(); }); // COM autoDispose: é limpo quando nenhum widget está mais usando final tempProvider = NotifierProvider.autoDispose<CounterNotifier, int>(() { return CounterNotifier(); });Quando usar autoDispose:
- ✅ Dados temporários (detalhes de produto, perfil de usuário visitado, etc)
- ✅ Dados específicos de uma tela que não precisam ficar na memória
- ❌ Dados globais que você quer manter (usuário logado, tema do app, carrinho de compras)
Crie métodos auxiliares para estados complexos: Se seu estado tem muitos campos, crie um método
copyWith:class CountersState { final int counter1; final int counter2; CountersState({required this.counter1, required this.counter2}); CountersState copyWith({int? counter1, int? counter2}) { return CountersState( counter1: counter1 ?? this.counter1, counter2: counter2 ?? this.counter2, ); } } // Uso: facilita atualizar apenas um campo state = state.copyWith(counter1: 10);
Conclusão
Riverpod transforma o gerenciamento de estado no Flutter em algo simples e poderoso. Com sua arquitetura sem BuildContext, segurança de tipos e facilidade de testes, ele se tornou a escolha preferida de muitos desenvolvedores Flutter modernos.
O exemplo que construímos demonstra um dos poderes mais importantes do gerenciamento de estado: compartilhar estado entre páginas diferentes sem passar parâmetros. As duas páginas acessam o mesmo provider através do ref e reagem automaticamente às mudanças. Isso é gerenciamento de estado na prática!
A partir daqui, você tem uma base sólida para construir aplicações Flutter escaláveis e bem estruturadas. Experimente adicionar mais funcionalidades ao app, como salvar o contador no SharedPreferences ou criar múltiplos contadores independentes. O código está pronto para evoluir!
Recursos Adicionais: