Riverpod for Beginners: Your First Provider, Notifier and AsyncValue

Riverpod was built by the same author as Provider, as a redesign that fixes Provider’s biggest gaps: no compile-time safety for missing providers, no built-in handling for async loading/error states, and awkward testing. If you already know Provider, most of the concepts here will feel familiar under new names.

Setup

dependencies:
  flutter_riverpod: ^2.5.0

Wrap your app in a ProviderScope — this is the one mandatory setup step, and it replaces the need to wire up a provider tree manually the way Provider requires:

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

Your First Provider

A simple, read-only value uses Provider. For state that changes, use NotifierProvider with a Notifier class:

class Counter extends Notifier<int> {
  @override
  int build() => 0;

  void increment() => state++;
}

final counterProvider = NotifierProvider<Counter, int>(Counter.new);

build() returns the initial state. Anywhere else in the notifier, assigning to state both updates the value and notifies listeners — there’s no separate notifyListeners() call to remember, unlike ChangeNotifier.

Reading It in a Widget

class CounterView extends ConsumerWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return ElevatedButton(
      onPressed: () => ref.read(counterProvider.notifier).increment(),
      child: Text('$count'),
    );
  }
}

Widgets that read providers extend ConsumerWidget instead of StatelessWidget, which supplies the WidgetRef ref parameter. Same watch/read distinction as Provider: ref.watch() inside build() to rebuild on change, ref.read() inside callbacks to act without subscribing.

The Big Advantage: AsyncValue

For data that loads asynchronously — an API call, a database query — use AsyncNotifier, whose state is automatically wrapped in an AsyncValue that models loading, data, and error states without you having to build that logic yourself:

class UserNotifier extends AsyncNotifier<User> {
  @override
  Future<User> build() async {
    return await repository.fetchUser();
  }
}

final userProvider = AsyncNotifierProvider<UserNotifier, User>(UserNotifier.new);

// In the widget:
final userAsync = ref.watch(userProvider);

userAsync.when(
  loading: () => const CircularProgressIndicator(),
  error: (err, stack) => Text('Error: $err'),
  data: (user) => Text(user.name),
);

This is the feature Provider has no direct equivalent for — with plain ChangeNotifier, you’d manually track a loading boolean and an error field yourself.

Should You Choose Riverpod Over Provider?

For a new project, Riverpod is generally the better default — it catches missing-provider mistakes at compile time rather than runtime, and the AsyncValue pattern removes a lot of repetitive loading/error boilerplate. Provider’s main advantage is a slightly gentler learning curve if you already know ChangeNotifier from elsewhere in the Flutter SDK.