Flutter: Gerenciamento de Estado — Riverpod
O estado é o sangue vital de um app — mal gerenciado, causa "sangramento interno": inconsistência de dados, tempestades de rebuild, vazamentos de memória.
📋 Pré-requisitos: Você precisa dominar o seguinte primeiro
- Aula 5: StatefulWidget e Interação
- Aula 11: Rede e API REST
1. O Que Você Vai Aprender
- Sistema Provider: Provider / StateProvider / FutureProvider / StreamProvider / NotifierProvider
- Riverpod 2.x: Notifier / AsyncNotifier / anotação @riverpod geração de código
- ref.watch / ref.read / ref.listen cenários de uso e compensações de desempenho
- ProviderScope e ConsumerWidget / ConsumerStatefulWidget
- ShopApp: CartNotifier para carrinho de compras + AsyncProductNotifier para carregamento assíncrono de produtos
2. Uma História Real de um Desastre de Estado
(1) O Problema: O Dilema do Estado Global com setState
O ShopApp do Bob usa setState para gerenciar o estado do carrinho de compras. O problema: os dados do carrinho são necessários nas páginas Home, Detail e Cart, exigindo callbacks para passá-los por cada camada da árvore de widgets. Após CartPage alterar a quantidade, o badge no AppBar de HomePage não atualiza — porque o State de HomePage não sabe o que CartPage mudou. Pior, 10 páginas mantêm 5 cópias dos dados do carrinho, todas dessincronizadas.
(2) A Solução com Riverpod
Riverpod eleva o estado para Providers globais. Qualquer página pode fazer ref.watch do mesmo estado do carrinho, e uma mudança em um lugar atualiza automaticamente todos os listeners.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ Dev dependency: flutter pub add --dev riverpod_generator build_runner
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - CartItem: see CartNotifier example below
// Global cart state - any page can access
@riverpod
class CartNotifier extends _$CartNotifier {
@override
List<CartItem> build() => [];
void addItem(Product product) {
state = [...state, CartItem(product: product, quantity: 1)];
}
void removeItem(int productId) {
state = state.where((item) => item.product.id != productId).toList();
}
}
// Watch in any widget
final cart = ref.watch(cartNotifierProvider);
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Benefício: Uma Mudança, Sincronização Global
Após usar Riverpod, o estado do carrinho do Bob vive em um único lugar — qualquer mudança em qualquer página atualiza automaticamente todos os listeners. O inferno dos callbacks desaparece, e os dados permanecem consistentes.
3. Conceitos Centrais do Riverpod
graph TD
PS[ProviderScope] --> CN[CartNotifier]
PS --> PN[ProductAsyncNotifier]
PS --> UN[UserNotifier]
CN --> |ref.watch| CV[CartView]
PN --> |ref.watch| PV[ProductListView]
UN --> |ref.watch| UV[UserProfileView]
CN --> |totalItems| Badge[BottomNav Badge]
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(1) Tipos de Provider
| Provider | Tipo de Estado | Caso de Uso |
|---|---|---|
Provider |
Valor imutável | Injeção de dependência, configuração |
StateProvider |
Valor mutável simples | Contador, toggle |
FutureProvider |
Resultado Future | Dados assíncronos únicos |
StreamProvider |
Resultado Stream | Fluxo de dados em tempo real |
NotifierProvider |
Instância Notifier | Estado complexo + lógica de negócio |
AsyncNotifierProvider |
AsyncNotifier | Inicialização assíncrona + lógica de negócio |
(2) Comparação dos Métodos ref
| Método | Propósito | Dispara Rebuild | Onde Usar |
|---|---|---|---|
ref.watch |
Escutar mudanças de estado | Sim (quando o estado muda) | Dentro do método build |
ref.read |
Leitura única | Não | Callbacks / event handlers |
ref.listen |
Escutar + executar efeitos colaterais | Não | Dentro do build (navegação/SnackBar) |
4. Providers Escritos Manualmente
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: StateProvider estado simples
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Simple counter with StateProvider
final counterProvider = StateProvider<int>((ref) => 0);
// Theme mode provider
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
// Usage in widget
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Text('Increment'),
),
],
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: FutureProvider dados assíncronos
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - ProductTile: custom Widget for displaying a single product
// - productRepositoryProvider: see Lesson 14 ProductRepository
// Load products once
final productsProvider = FutureProvider<List<Product>>((ref) async {
final repo = ref.read(productRepositoryProvider);
return repo.getProducts();
});
// Usage
class ProductList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, _) => Text('Error: $err'),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(product: products[i]),
),
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
5. Notifier e AsyncNotifier
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: CartNotifier (lógica completa do carrinho de compras)
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model (simplified version below)
/*
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
*/
class CartItem {
final Product product;
final int quantity;
const CartItem({required this.product, required this.quantity});
double get total => product.price * quantity;
CartItem copyWith({int? quantity}) => CartItem(product: product, quantity: quantity ?? this.quantity);
}
class CartNotifier extends Notifier<List<CartItem>> {
@override
List<CartItem> build() => [];
void addItem(Product product) {
final idx = state.indexWhere((i) => i.product.id == product.id);
if (idx >= 0) {
state = [...state]..[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
} else {
state = [...state, CartItem(product: product, quantity: 1)];
}
}
void updateQuantity(int productId, int quantity) {
if (quantity <= 0) {
removeItem(productId);
return;
}
state = [
for (final item in state)
if (item.product.id == productId) item.copyWith(quantity: quantity) else item,
];
}
void removeItem(int productId) {
state = state.where((i) => i.product.id != productId).toList();
}
void clear() => state = [];
}
final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>(CartNotifier.new);
// Derived state: total price
final cartTotalProvider = Provider<double>((ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.total);
});
// Derived state: item count
final cartItemCountProvider = Provider<int>((ref) {
final items = ref.watch(cartProvider);
return items.fold(0, (sum, item) => sum + item.quantity);
});
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: AsyncNotifier (inicialização assíncrona de produtos)
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - productRepositoryProvider: see Lesson 14 ProductRepository
class ProductAsyncNotifier extends AsyncNotifier<List<Product>> {
@override
Future<List<Product>> build() async {
final repo = ref.read(productRepositoryProvider);
return repo.getProducts();
}
Future<void> loadMore() async {
final current = state.valueOrNull ?? [];
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final more = await ref.read(productRepositoryProvider).getProducts(page: (current.length ~/ 20) + 1);
return [...current, ...more];
});
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => ref.read(productRepositoryProvider).getProducts());
}
}
final productProvider = AsyncNotifierProvider<ProductAsyncNotifier, List<Product>>(
ProductAsyncNotifier.new,
);
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
6. Geração de Código com @riverpod
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Usando riverpod_generator
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ Dev dependency: flutter pub add --dev riverpod_generator build_runner
// Custom class definition sources:
// - CartItem: see Lesson 5 CartNotifier example in this lesson
// - User/AuthService: see Lesson 14 Auth Notifier
part 'providers.g.dart';
// Generated notifier
@riverpod
class Cart extends _$Cart {
@override
List<CartItem> build() => [];
void addItem(Product product) {
final idx = state.indexWhere((i) => i.product.id == product.id);
if (idx >= 0) {
state[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
state = [...state]; // Trigger rebuild
} else {
state = [...state, CartItem(product: product, quantity: 1)];
}
}
}
// Generated provider (derived)
@riverpod
double cartTotal(CartTotalRef ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.total);
}
// Keep alive across widget lifecycle
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
@override
AsyncValue<User?> build() => const AsyncData(null);
Future<void> login(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => AuthService.login(email, password));
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
7. Exemplo Completo: Integração Riverpod no ShopApp
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod go_router
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - cartProvider/cartTotalProvider/cartItemCountProvider: see Lesson 5 in this lesson
// - themeModeProvider: see Lesson 4 StateProvider in this lesson
// main.dart
void main() {
runApp(ProviderScope(child: ShopApp()));
}
class ShopApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
return MaterialApp.router(
title: 'ShopApp',
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
darkTheme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true, brightness: Brightness.dark),
themeMode: themeMode,
routerConfig: router,
);
}
}
// Cart badge in AppBar - auto-updates
class CartBadge extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(cartItemCountProvider);
return Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shopping_cart),
if (count > 0)
Positioned(right: 0, top: 0,
child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
child: Text('$count', style: const TextStyle(fontSize: 9, color: Colors.white)))),
],
);
}
}
// Add to cart action
class AddToCartButton extends ConsumerWidget {
final Product product;
const AddToCartButton({super.key, required this.product});
@override
Widget build(BuildContext context, WidgetRef ref) {
return FilledButton.icon(
onPressed: () {
ref.read(cartProvider.notifier).addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart')),
);
},
icon: const Icon(Icons.shopping_cart),
label: const Text('Add to Cart'),
);
}
}
// Cart page with Riverpod
class CartPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartProvider);
final total = ref.watch(cartTotalProvider);
return Scaffold(
appBar: AppBar(title: Text('Cart (${items.length})')),
body: items.isEmpty
? const Center(child: Text('Cart is empty'))
: Column(children: [
Expanded(child: ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (_, i) {
final item = items[i];
return ListTile(
title: Text(item.product.name),
subtitle: Text('\$${item.total.toStringAsFixed(2)}'),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(icon: const Icon(Icons.remove), onPressed: () =>
ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity - 1)),
Text('${item.quantity}'),
IconButton(icon: const Icon(Icons.add), onPressed: () =>
ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity + 1)),
]),
);
},
)),
Padding(padding: const EdgeInsets.all(16),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text('\$${total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
FilledButton(onPressed: () => context.push('/checkout'), child: const Text('Checkout')),
])),
]),
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: Qual a diferença entre Riverpod e o pacote Provider? R: Riverpod é a evolução do Provider, resolvendo os problemas de injeção de dependência insegura e testes do Provider. Use Riverpod para novos projetos.
P: Como escolher entre ref.watch e ref.read? R: Use ref.watch dentro do build (quando a UI precisa responder a mudanças de estado); use ref.read em callbacks/event handlers (sem necessidade de rebuild).
P: state = [...state] é necessário? R: Sim. Riverpod usa verificações de identidade para detectar mudanças de estado. Modificar o conteúdo de uma List não cria um novo objeto, então Riverpod não notificará as atualizações.
P: Como lidar com AsyncLoading/AsyncData/AsyncError no AsyncNotifier? R: Use o pattern matching
.when(loading:, error:, data:)para exibir indicadores de carregamento, páginas de erro e conteúdo de dados respectivamente.
P: Quais dependências a anotação @riverpod precisa? R: Adicione riverpod_annotation + riverpod_generator + build_runner ao pubspec.yaml, depois execute
dart run build_runner watch.
P: Qual a diferença entre keepAlive e autoDispose? R: autoDispose (padrão) destrói o provider quando ninguém está escutando; keepAlive nunca o destrói. Use keepAlive para estado global (como Auth).
📖 Resumo
- Riverpod gerencia estado global de forma uniforme — uma mudança atualiza todos os listeners automaticamente
- Notifier serve para estado complexo + lógica de negócio; AsyncNotifier serve para inicialização assíncrona
- ref.watch escuta no build, ref.read opera em callbacks, ref.listen trata efeitos colaterais
- Providers derivados (cartTotalProvider) computam automaticamente o estado dependente
- Geração de código @riverpod reduz boilerplate — recomendado para novos projetos
📝 Exercícios
- Básico (⭐): Use StateProvider para implementar alternância de modo escuro, e exiba o modo atual no AppBar com ConsumerWidget.
- Intermediário (⭐⭐): Use Notifier para implementar CartNotifier com adicionar/remover/atualizar e cálculo de preço total, exibindo o estado do carrinho em duas páginas diferentes.
- Desafio (⭐⭐⭐): Use geração de código @riverpod para implementar Auth + Product + Cart Notifiers: após login, carregar produtos automaticamente; após adicionar ao carrinho, o badge atualiza automaticamente.