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

1. O Que Você Vai Aprender


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.

DART
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);
TEXT
> 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

100%
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]
TEXT
> 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)
⚠️ Nota: Não use ref.watch fora do build, e não use ref.read para escutar estado dentro do build.


4. Providers Escritos Manualmente

▶ Exemplo

TEXT
> 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

DART
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'),
        ),
      ],
    );
  }
}
TEXT
> 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

TEXT
> 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

DART
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]),
      ),
    );
  }
}
TEXT
> 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

TEXT
> 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)

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 (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);
});
TEXT
> 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

TEXT
> 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)

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
// - 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,
);
TEXT
> 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

TEXT
> 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

DART
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));
  }
}
TEXT
> 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

DART
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')),
                ])),
            ]),
    );
  }
}
TEXT
> 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


📝 Exercícios

  1. Básico (⭐): Use StateProvider para implementar alternância de modo escuro, e exiba o modo atual no AppBar com ConsumerWidget.
  2. 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.
  3. 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.

← Aula Anterior | Próxima Aula →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%