Irbisa · Cheatsheet

State with Riverpod

Riverpod providers, AsyncNotifier, ref, code generation

Senior Developer · 25 questions

  1. 01

    Which provider types does Riverpod offer, and when do you pick Notifier, AsyncNotifier or a plain FutureProvider?

    Pick by how the state is produced and whether anyone changes it: Provider for a computed or injected value, FutureProvider or StreamProvider for a read-only async value, Notifier for synchronous state with methods, AsyncNotifier for async state the user also changes.

  2. 02

    What is the difference between ref.watch, ref.read and ref.listen?

    watch subscribes and rebuilds, read takes the current value once without subscribing, and listen subscribes but runs a callback instead of rebuilding.

    class HomePage extends ConsumerWidget {  const HomePage({super.key});   @override  Widget build(BuildContext context, WidgetRef ref) {    // watch: this widget rebuilds when the count changes…
  3. 03

    What is the difference between ConsumerWidget, ConsumerStatefulWidget and the Consumer builder?

    All three give you a WidgetRef.

    class ProductPage extends StatelessWidget {  const ProductPage({super.key});   @override  Widget build(BuildContext context) {    return Scaffold(…
  4. 04

    What is the difference between ref.invalidate and ref.refresh?

    ref.invalidate(p) marks the provider stale so it rebuilds, at once if something listens and on the next read otherwise; ref.refresh(p) does the same but rebuilds immediately and returns the new value.

  5. 05

    What is AsyncValue, and how do you render its loading, error and data states?

    AsyncValue<T> is the sealed type that async providers expose: AsyncData, AsyncLoading or AsyncError.

    class ProfileView extends ConsumerWidget {  const ProfileView({super.key});   @override  Widget build(BuildContext context, WidgetRef ref) {    final profile = ref.watch(profileProvider);…
  6. 06

    What is the family modifier, and what must be true of its argument?

    family turns one declaration into a set of providers keyed by an argument: productProvider(42) and productProvider(7) are separate instances with separate cached state.

    final productProvider = FutureProvider.autoDispose.family<Product, int>(  (ref, id) => ref.watch(productRepositoryProvider).getProduct(id),); // A family notifier receives its argument through the constructor (Riverpod 3)class Basket extends Notifier<List<Item>> {…
  7. 07

    How does autoDispose work, and when do you need ref.keepAlive()?

    By default a provider lives as long as its container.

    final searchProvider =    FutureProvider.autoDispose.family<List<Hit>, String>((ref, query) async {  // keep results for 5 minutes after the last listener leaves  final link = ref.keepAlive();  Timer? timer;  ref.onCancel(() {…
  8. 08

    How does Riverpod code generation (@riverpod) work, and why use it?

    riverpod_generator is a build_runner step: you annotate a function or a class with @riverpod, and it writes the provider declaration, choosing the provider type from the signature.

    import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'products.g.dart'; // A function: generates productProvider, a FutureProvider family@riverpod…
  9. 09

    How do you limit rebuilds with select, and why does it depend on equality?

    ref.watch(p.select((s) => s.field)) subscribes to one derived value and rebuilds only when that value changes by ==.

    // UiState is a freezed class: theme, sidebarOpen, unreadCount // wrong: rebuilds on every unread-count tickclass ThemeIconBad extends ConsumerWidget {  const ThemeIconBad({super.key});  @override…
  10. 10

    Neither add nor addAgain updates the cart badge on screen. What is Riverpod doing, and how do you fix it?

    class Cart extends Notifier<List<Item>> {  @override  List<Item> build() => [];   void add(Item item) {    state.add(item);  }   void addAgain(Item item) {    state = state..add(item);  }} final cartProvider = NotifierProvider<Cart, List<Item>>(Cart.new);

    Riverpod notifies listeners only when you assign a new state that is not == to the old one.

  11. 11

    This results screen fires a new search request on every rebuild and never leaves the loading state. What is wrong?

    class SearchQuery {  SearchQuery(this.text, this.page);  final String text;  final int page;} final searchProvider = FutureProvider.autoDispose    .family<List<Hit>, SearchQuery>(  (ref, q) => ref.watch(apiProvider).search(q.text, q.page),); // inside ResultsView.build(context, ref):final hits = ref.watch(searchProvider(SearchQuery(text, 1)));

    SearchQuery has no == or hashCode, so every build creates a key that matches no existing family member.

  12. 12

    Find the bug: tapping a filter chip highlights it, but the list only switches to the new filter after the user toggles a todo.

    final filterProvider =    NotifierProvider<FilterNotifier, Filter>(FilterNotifier.new); final visibleTodosProvider = Provider<List<Todo>>((ref) {  final todos = ref.watch(todosProvider);  final filter = ref.read(filterProvider);  return switch (filter) {    Filter.all => todos,    Filter.active => todos.where((t) => !t.done).toList(),    Filter.done => todos.where((t) => t.done).toList(),  };}); // a chip's onSelected: ref.read(filterProvider.notifier).set(f)

    visibleTodosProvider reads the filter with ref.read, which returns the value once and subscribes to nothing.

  13. 13

    How do you trigger navigation, snackbars or dialogs from Riverpod state?

    With ref.listen in a widget.

    class CheckoutPage extends ConsumerWidget {  const CheckoutPage({super.key});   @override  Widget build(BuildContext context, WidgetRef ref) {    ref.listen(orderProvider, (prev, next) {…
  14. 14

    A screen writes a provider's state from initState and Riverpod throws "Tried to modify a provider while the widget tree was building". Where does that write belong?

    initState runs during the build phase, and a provider changing there would have to notify widgets that this frame has already built, so Riverpod throws instead of rendering a tree with two different truths.

    // wrong: throws "Tried to modify a provider while the widget tree was building"class _FilterScreenState extends ConsumerState<FilterScreen> {  @override  void initState() {    super.initState();    ref.read(filterProvider.notifier).set(widget.initial);…
  15. 15

    A provider needs data from two other async providers before it can compute anything. How do you write it without nesting when() calls?

    Await the sources' futures inside the derived provider instead of matching two AsyncValue objects against each other.

    final userProvider = FutureProvider<User>((ref) => ref.watch(apiProvider).me());final settingsProvider =    FutureProvider<Settings>((ref) => ref.watch(apiProvider).settings()); // right: await .future, and one AsyncValue comes outfinal dashboardProvider = FutureProvider<Dashboard>((ref) async {…
  16. 16

    Toggling a todo makes the whole list flash a full-screen spinner although the old rows are still in memory. Why, and how do you keep them on screen?

    The old rows are still inside the AsyncValue; the spinner appears because the update was marked as a reload, and when() renders its loading branch on a reload by default.

    class TodosNotifier extends AsyncNotifier<List<Todo>> {  @override  Future<List<Todo>> build() => ref.watch(apiProvider).fetchTodos();   Future<List<Todo>> _toggle(Todo t) async {    await ref.read(apiProvider).setDone(t.id, !t.done);…
  17. 17

    How do you unit-test a Riverpod notifier with ProviderContainer, and why can its state reset between two calls?

    Create a ProviderContainer with overrides for the dependencies, keep a listener on the provider under test, and drive the real notifier through container.read(p.notifier).

    class Cart extends AsyncNotifier<List<Item>> {  @override  Future<List<Item>> build() => ref.watch(cartRepoProvider).load();   Future<void> addItem(Item i) async {    await ref.read(cartRepoProvider).save(i);…
  18. 18

    QA says the cart total goes wrong after one specific navigation and nobody can reproduce it. How do you make Riverpod tell you which provider changed and when?

    Attach a ProviderObserver to the root ProviderScope.

    class StateLogger extends ProviderObserver {  // Riverpod 3 signature; 2.x passed (provider, previous, next, container)  @override  void didUpdateProvider(    ProviderObserverContext context,    Object? previousValue,…
  19. 19

    How do ProviderScope overrides work, and what does it take to scope a provider to a subtree?

    An override replaces what a provider resolves to inside one container: the root ProviderScope for app-wide swaps and test fakes, a nested ProviderScope for a subtree.

    // Root: per-flavour config and test fakesvoid main() {  runApp(ProviderScope(    overrides: [      apiBaseUrlProvider.overrideWithValue(const String.fromEnvironment('API_URL')),    ],…
  20. 20

    Your AsyncNotifier awaits a slow request and then touches ref; the user pops the screen mid-flight. What breaks, and how do you write it safely?

    When the screen is popped, the autoDispose provider is disposed, but your await keeps going.

    class UploadNotifier extends AsyncNotifier<Upload> {  @override  Future<Upload> build() async {    final token = CancelToken();    ref.onDispose(token.cancel); // an abandoned request dies with the provider…
  21. 21

    A user is halfway through editing a form held in an AsyncNotifier when the session token refreshes, and their edits vanish. What happened?

    The notifier watched the session inside build(), so the token refresh threw the state away and re-ran build() from scratch.

  22. 22

    After the Riverpod 3 upgrade, a screen whose request returns 400 keeps re-firing that request every few seconds. What is doing that, and how do you stop it?

    Riverpod 3 retries failed providers automatically: by default up to 10 times, with exponential backoff from 200 ms to 6.4 s.

    Duration? retryPolicy(int retryCount, Object error) {  if (retryCount >= 3) return null; // give up, show the error UI  if (error is ProviderException) return null; // the source retries itself   final transient = switch (error) {    DioException(:final response?) => (response.statusCode ?? 0) >= 500,…
  23. 23

    What changed in Riverpod 3, and what breaks when you upgrade a 2.x app?

    Riverpod 3.0 (September 2025) folded the API into plain Notifier and AsyncNotifier, made Ref untyped, and changed runtime behaviour: == filtering everywhere, automatic retry, paused listeners for hidden widgets, and a Ref that throws once disposed.

  24. 24

    A foreground push handler and your main() bootstrap both need the repositories the app reads through providers. How do you share them without two copies of the state?

    Create the ProviderContainer yourself, use it from plain Dart, and hand that same container to the widget tree with UncontrolledProviderScope.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();   // one container: built in main(), warmed up, then handed to the tree  final container = ProviderContainer(    overrides: […
  25. 25

    Riverpod asks you to declare providers as top-level final variables. Isn't that exactly the global mutable state everyone warns about?

    The global holds no state.