Flutter Riverpod Basics

Riverpod is a state management library created by the author of Provider. It fixes common Provider pain points: it needs no BuildContext to read data, it catches mistakes at compile time, and it runs the same way in tests. Think of Riverpod as a well-organized warehouse where every item has a label, and any worker can fetch an item by its label.

Installing Riverpod


flutter pub add flutter_riverpod

Check the package page on pub.dev for the newest syntax, because the library receives regular updates.

The Big Picture


  ProviderScope (the warehouse)
   │
   ├── counterProvider  → holds an int
   ├── nameProvider     → holds a String
   └── todosProvider    → holds a List
            ▲
            │ ref.watch / ref.read
            │
        Your widgets

A provider is a small object that creates and stores one piece of state. A ref is the handle a widget uses to reach providers.

Step 1: Wrap the App

Place ProviderScope at the top of the widget tree. It stores all provider state.


import 'package:flutter_riverpod/flutter_riverpod.dart';

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

Step 2: Create a Simple Provider

A plain Provider exposes a value that never changes, such as a settings object or a greeting.


final greetingProvider = Provider<String>((ref) {
  return 'Welcome to Flutter';
});

Declare providers as global final variables. The variable itself holds no state; it acts only as a key.

Step 3: Read It in a Widget

Extend ConsumerWidget instead of StatelessWidget. The build method receives an extra WidgetRef parameter.


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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final message = ref.watch(greetingProvider);
    return Text(message);
  }
}

The ref.watch call rebuilds the widget whenever the provider value changes.

Changing State with NotifierProvider

A counter needs state that changes. Write a Notifier class that holds the value in a property named state.


class CounterNotifier extends Notifier<int> {
  @override
  int build() {
    return 0;   // starting value
  }

  void increment() {
    state = state + 1;
  }

  void reset() {
    state = 0;
  }
}

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

Assigning a new value to state notifies every widget that watches the provider.

Using the Counter


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

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

    return Scaffold(
      body: Center(child: Text('Count: $count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).increment();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

  Tap button
      │
      ▼
  ref.read(counterProvider.notifier).increment()
      │
      ▼
  state changes 0 → 1
      │
      ▼
  ref.watch listeners rebuild → Text shows "Count: 1"

watch and read

MethodWhere to UseBehavior
ref.watchInside buildRebuilds when the value changes
ref.readInside button handlersReads once with no rebuild

Never call ref.read inside build to get displayed data. The screen would miss every update.

FutureProvider for Loading Data

A FutureProvider handles network calls and exposes three states: loading, data, and error.


final quoteProvider = FutureProvider<String>((ref) async {
  await Future.delayed(Duration(seconds: 1));
  return 'Practice makes progress';
});

class QuoteView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final quote = ref.watch(quoteProvider);
    return quote.when(
      data: (text) => Text(text),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('Something went wrong'),
    );
  }
}

The when method forces you to handle all three states, which prevents blank screens.

Providers That Depend on Providers

One provider reads another through its own ref. Riverpod recalculates the dependent provider automatically.


final doubleCountProvider = Provider<int>((ref) {
  final count = ref.watch(counterProvider);
  return count * 2;
});

Why Developers Choose Riverpod

  • Providers work outside the widget tree, in plain Dart code.
  • The compiler catches missing providers before the app runs.
  • Tests replace any provider with fake data in one line.
  • Async data has built-in loading and error handling.

Choosing a Provider Type

NeedProvider
Fixed value or computed valueProvider
Value with methods that change itNotifierProvider
One async result, such as a network callFutureProvider
Live data from a streamStreamProvider

Practice Ideas

Build a to-do list with a NotifierProvider holding a list of strings. Add methods named add and remove. Show the count of tasks in the app bar through a second provider that reads the first. Watch both widgets update from one change.

Testing with Riverpod

Testing a screen that reads a provider takes one extra step: wrap the widget in a ProviderScope and override the provider with fake data. A weather screen that normally calls a server shows a fixed temperature during the test. No network, no waiting, and no random failures. This ease of testing is a major reason teams adopt the library for projects that grow beyond a few screens.