Flutter: State Management — Riverpod

State is the lifeblood of an app — mismanaged, it causes "internal bleeding": data inconsistency, rebuild storms, memory leaks.

📋 Prerequisites: You need to master the following first

1. What You Will Learn


2. A True Story of a State Disaster

(1) Pain Point: The Global State Dilemma with setState

Bob's ShopApp uses setState to manage shopping cart state. The problem: cart data is needed on the Home, Detail, and Cart pages, requiring callbacks to pass it through every layer of the widget tree. After CartPage changes the quantity, the AppBar badge on HomePage doesn't update — because HomePage's State doesn't know what CartPage changed. Worse, 10 pages hold 5 copies of cart data, all out of sync.

(2) The Riverpod Solution

Riverpod lifts state to global Providers. Any page can ref.watch the same cart state, and a change in one place automatically updates all 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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

(3) Benefit: One Change, Global Sync

After using Riverpod, Bob's cart state lives in a single place — any page's changes automatically update all listeners. Callback hell disappears, and data stays consistent.


3. Riverpod Core Concepts

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

(1) Provider Types

Provider State Type Use Case
Provider Immutable value Dependency injection, configuration
StateProvider Simple mutable value Counter, toggle
FutureProvider Future result One-time async data
StreamProvider Stream result Real-time data stream
NotifierProvider Notifier instance Complex state + business logic
AsyncNotifierProvider AsyncNotifier Async initialization + business logic

(2) ref Method Comparison

Method Purpose Triggers Rebuild Where to Use
ref.watch Listen to state changes Yes (when state changes) Inside build method
ref.read One-time read No Callbacks / event handlers
ref.listen Listen + execute side effects No Inside build (navigation/SnackBar)
⚠️ Note: Do not use ref.watch outside build, and do not use ref.read to listen to state inside build.


4. Hand-Written Providers

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

: StateProvider simple state

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

: FutureProvider async data

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

5. Notifier and AsyncNotifier

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

: CartNotifier (complete shopping cart logic)

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

: AsyncNotifier (async product initialization)

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

6. @riverpod Code Generation

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

: Using 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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

7. Complete Example: ShopApp Riverpod Integration

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.

❓ FAQ

Q What's the difference between Riverpod and the Provider package?
A Riverpod is the evolution of Provider, solving Provider's unsafe dependency injection and testing issues. Use Riverpod for new projects.
Q How to choose between ref.watch and ref.read?
A Use ref.watch inside build (when UI needs to respond to state changes); use ref.read in callbacks/event handlers (no rebuild needed).
Q Is state = [...state] necessary?
A Yes. Riverpod uses identical checks to detect state changes. Modifying List contents doesn't create a new object, so Riverpod won't notify updates.
Q How to handle AsyncLoading/AsyncData/AsyncError in AsyncNotifier?
A Use the .when(loading:, error:, data:) pattern matching to display loading indicators, error pages, and data content respectively.
Q What dependencies does the @riverpod annotation need?
A Add riverpod_annotation + riverpod_generator + build_runner to pubspec.yaml, then run dart run build_runner watch.
Q What's the difference between keepAlive and autoDispose?
A autoDispose (default) destroys the provider when no one is listening; keepAlive never destroys it. Use keepAlive for global state (like Auth).

📖 Summary


📝 Exercises

  1. Basic (⭐): Use StateProvider to implement dark mode switching, and display the current mode in the AppBar with ConsumerWidget.
  2. Intermediate (⭐⭐): Use Notifier to implement CartNotifier with add/remove/update and total price calculation, displaying cart state on two different pages.
  3. Challenge (⭐⭐⭐): Use @riverpod code generation to implement Auth + Product + Cart Notifiers: after login, auto-load products; after adding to cart, badge auto-updates.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏