Core Concepts

  • Purpose: Riverpod is used for global state (shared across the app), while Hooks are used for local state and managing widget lifecycles (like controllers and timers) without needing a StatefulWidget.
  • The Key Widget: To use both in the same widget, you must extend HookConsumerWidget.
    • This gives you access to both the WidgetRef (to talk to Riverpod) and the ability to use hooks like useState or useEffect.

When to Use Each

FeatureUse Riverpod (Providers)Use Flutter Hooks
State ScopeGlobal / Shared across screensLocal to a single widget
ExamplesAuth status, User Profile, API dataText controllers, Animation controllers, Scroll controllers
BenefitTestable, decoupled logicRemoves “initState” and “dispose” boilerplate

Basic Usage Example

Using HookConsumerWidget allows you to combine global providers with local hooks:

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

// A simple global provider
final counterProvider = StateProvider((ref) => 0);

class MyWidget extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1. Riverpod: Watch global state
    final count = ref.watch(counterProvider);

    // 2. Hooks: Manage local state (no StatefulWidget needed)
    final textController = useTextEditingController();

    return Column(
      children: [
        Text('Global Count: $count'),
        TextField(controller: textController),
      ],
    );
  }
}

Important Constraints

  1. Call Order: Hooks must be called at the top level of the build method.
    1. They cannot be called inside loops, conditions (if statements), or asynchronous blocks.
  2. Inheritance: If you use a hook, your widget must extend HookWidget or HookConsumerWidget.
    1. Using a hook in a standard StatelessWidget will cause a runtime crash.

Setup & Imports

  • To use code generation alongside hooks, ensure your widget extends HookConsumerWidget.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

// Required for Riverpod code generation boilerplate
part 'my_widget.g.dart';

Modern Syntax (@riverpod & keepAlive)

  • The @riverpod annotation automatically handles provider types based on your function/class structure.
  • By default, auto-generated providers are auto-dispose (they destroy their state when no widgets are listening).

Auto-Dispose Provider (Default)

@riverpod
String publicData(Ref ref) => 'Hello World';
// Generates: publicDataProvider

Keep-Alive Provider (Never Disposed)

  • Pass keepAlive: true to preserve the provider state in memory even if no widgets are actively listening to it.
@riverpod(keepAlive: true)
class UserSession extends _$UserSession {
  @override
  String build() => 'Logged In';
}
// Generates: userSessionProvider

Managing State inside @riverpod

  • Choose between functions for read-only/async data, or classes for data that needs mutations.

Read-Only / Async State (Functions)

@riverpod
Future<UserData> fetchUser(Ref ref, {required String id}) async {
  return ref.read(apiServiceProvider).getUser(id);
}
// Generates: fetchUserProvider(id: '123')

Mutable State (Notifier Classes)

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0; // Initial state

  void increment() => state++; // Update state
}
// Generates: counterProvider

Monitoring State in Hooks Widgets

  • Inside a HookConsumerWidget, use the WidgetRef ref parameter inside the build method alongside your local Flutter hooks.
class ProfileScreen extends HookConsumerWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // -------------------------------------------------------------
    // RIVERPOD: Monitoring Global State
    // -------------------------------------------------------------

    // 1. ref.watch : Rebuilds widget when the state changes (Best for UI)
    final counter = ref.watch(counterProvider);

    // 2. ref.listen : Runs side-effects (snackbars, dialogs) without rebuilding UI
    ref.listen<int>(counterProvider, (previous, next) {
      if (next == 10) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Max reached!')));
    });

    // 3. ref.read : Safe to use inside buttons/callbacks to trigger actions
    // Do NOT use ref.read directly in the build body

    // -------------------------------------------------------------
    // FLUTTER HOOKS: Monitoring Local Lifecycle/State
    // -------------------------------------------------------------

    // 1. useTextEditingController : Disposes itself automatically
    final textController = useTextEditingController();

    // 2. useEffect : Simulates initState, didUpdateWidget, and dispose
    useEffect(() {
      print('Widget was mounted');
      return () => print('Widget was disposed'); // Cleanup function
    }, const []); // Empty array means this runs only ONCE on mount

    return Scaffold(
      body: Column(
        children: [
          Text('Global Count: $counter'),
          TextField(controller: textController),
          ElevatedButton(
            onPressed: () => ref.read(counterProvider.notifier).increment(),
            child: const Text('Increment Global'),
          ),
        ],
      ),
    );
  }
}

Quick Reference Table

GoalTool / MethodScopeSide Effect Rule
Kill state when unusedDefault @riverpodGlobalAutomated cleanup
Keep state forever@riverpod(keepAlive: true)GlobalStays in memory
Rebuild UI on changeref.watch(provider)GlobalUse in build() body
Trigger dialogs/snackbarsref.listen(provider, (p, n) {})GlobalUse in build() body
Fire a click actionref.read(provider.notifier).doX()GlobalUse inside callbacks only
Local state containeruseState(initialValue)LocalDisposed with widget
Lifecycle side effectsuseEffect(() => cleanup, [keys])LocalRe-runs when keys change