← Back to blog
Mastering Riverpod: Foundation · Part 4 of 6
August 5, 20268 min read

Your First Provider: Provider and ConsumerWidget Explained

RiverpodFlutterDart

Your First Provider

This is Part 4 of Mastering Riverpod: Foundation. You know why Riverpod (Part 1), how to set it up (Part 2), and where state lives (Part 3). Now the fun part: declaring a provider and reading it in the UI. By the end you'll have written the everyday Riverpod pattern you'll use in essentially every screen.

We'll start with the simplest provider — Provider — and the widget that reads it — ConsumerWidget.


Declaring a Provider

A Provider exposes a read-only value. You declare it as a top-level final variable, passing a function that receives a ref and returns the value:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// A top-level provider exposing a read-only String.
final greetingProvider = Provider<String>((ref) {
  return 'Hello, Riverpod';
});

Anatomy:

  • final greetingProvider — a global declaration (Part 3); by convention named <thing>Provider.
  • Provider<String> — the type tells you (and the compiler) it exposes a String.
  • (ref) { return ... } — the create function. It runs lazily the first time the provider is read, and its result is cached. The ref lets this provider read other providers (more below).

Provider is for values that don't change on their own — computed/derived values, configuration, and especially dependency injection (exposing a service/repository instance). For values that change over time, you'll use other provider types (the whole of Series 2).


Reading it with ConsumerWidget

To read a provider in the UI, you need a ref. The cleanest way is a ConsumerWidget — it's exactly like a StatelessWidget, but its build method gets an extra WidgetRef ref parameter:

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

  @override
  Widget build(BuildContext context, WidgetRef ref) { // ← note the extra `ref`
    final greeting = ref.watch(greetingProvider);     // read + subscribe
    return Scaffold(
      body: Center(child: Text(greeting)),            // shows "Hello, Riverpod"
    );
  }
}

Two things to internalize:

  1. ConsumerWidget replaces StatelessWidget when you need to read providers. Same idea, plus a ref.
  2. ref.watch(provider) reads the provider's current value and subscribes to it — so when the provider's value changes, this widget rebuilds automatically. (Full watch/read/listen breakdown is Part 5.)

That's the core loop of Riverpod UI: declare a provider, ref.watch it in a ConsumerWidget, render the value.


Consumer and ConsumerStatefulWidget — the other two ways to get a ref

ConsumerWidget is the common case, but there are three ways to obtain a ref, and it's worth knowing all three:

1. ConsumerWidget (most common)

A whole widget that's "Riverpod-aware." Use it when the widget needs providers throughout its build.

2. Consumer (scoped rebuilds)

A Consumer is a builder widget you drop into a normal widget tree to get a ref for just a portion of the UI. This is a performance tool: only the part inside Consumer rebuilds when the provider changes, not the whole widget.

class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveStaticHeader(),     // never rebuilds
        Consumer(
          builder: (context, ref, child) {
            final name = ref.watch(userNameProvider);
            return Text(name);              // only THIS rebuilds on change
          },
        ),
      ],
    );
  }
}

Use Consumer to narrow the rebuild scope — wrap only the widgets that actually depend on the provider, keeping expensive siblings static. (This connects to Flutter Part 4: smaller rebuild scope = better performance.)

3. ConsumerStatefulWidget (when you also need State)

When you need both a ref and classic State (e.g. an initState, a TextEditingController, an animation), use ConsumerStatefulWidget + ConsumerState. The ref is available throughout the State (not just build):

class SearchScreen extends ConsumerStatefulWidget {
  const SearchScreen({super.key});
  @override
  ConsumerState<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends ConsumerState<SearchScreen> {
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose(); // normal State lifecycle still applies
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final results = ref.watch(searchResultsProvider); // ref is a field here
    return /* ... */ const Placeholder();
  }
}

| You need… | Use | | --- | --- | | a whole widget that reads providers | ConsumerWidget | | a ref for just part of an existing widget (narrow rebuilds) | Consumer | | a ref and State (initState/controllers) | ConsumerStatefulWidget |


Providers that depend on other providers

Here's where Riverpod's "spreadsheet" reactivity (Part 1) shines. A provider's ref can watch other providers, deriving new state. When an upstream provider changes, the derived one recomputes — and any widget watching it rebuilds.

final firstNameProvider = Provider<String>((ref) => 'Vivek');
final lastNameProvider  = Provider<String>((ref) => 'Kumar');

// A derived provider that depends on the two above.
final fullNameProvider = Provider<String>((ref) {
  final first = ref.watch(firstNameProvider); // depend on first...
  final last = ref.watch(lastNameProvider);   // ...and last
  return '$first $last';
});
// If firstNameProvider ever changes, fullNameProvider recomputes,
// and every widget watching fullNameProvider rebuilds — automatically.

This is dependency injection and composition in one: fullNameProvider doesn't create names, it depends on the providers that do. You build your app as a graph of small providers, each watching the ones it needs — exactly the composability Riverpod was designed for.

Rule: use ref.watch inside a provider's create function to depend on another provider. Don't use ref.read there — it wouldn't re-run when the dependency changes (more in Part 5).


Provider as dependency injection

The single most common real-world use of plain Provider is exposing a service or repository so the rest of the app can depend on it without new-ing it everywhere:

// Expose a single shared instance of a service.
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());

final userRepositoryProvider = Provider<UserRepository>((ref) {
  final client = ref.watch(apiClientProvider); // inject the client
  return UserRepository(client);
});

Now any widget or provider gets the shared UserRepository via ref.watch(userRepositoryProvider) — and in tests you override it with a fake (Part 3). One declaration, injected everywhere, swappable for tests. That's Riverpod-style DI.


Practice Challenges

Challenge 1 — Declare and read. Declare a Provider<int> returning 42 and show it in a ConsumerWidget.

Show solution
final answerProvider = Provider<int>((ref) => 42);

class AnswerScreen extends ConsumerWidget {
  const AnswerScreen({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final answer = ref.watch(answerProvider);
    return Center(child: Text('$answer'));
  }
}

Challenge 2 — Narrow the rebuild. A page has an expensive header and a small live label. Show how to rebuild only the label when its provider changes.

Show solution
Column(children: [
  const ExpensiveHeader(),          // stays a StatelessWidget — never rebuilds
  Consumer(builder: (context, ref, _) {
    return Text(ref.watch(labelProvider)); // only this rebuilds
  }),
]);

Consumer scopes the rebuild to just the label.

Challenge 3 — Derive state. Given priceProvider and quantityProvider, write a totalProvider.

Show solution
final totalProvider = Provider<int>((ref) {
  final price = ref.watch(priceProvider);
  final qty = ref.watch(quantityProvider);
  return price * qty;
});

ref.watch inside the provider makes totalProvider recompute when either dependency changes.

Challenge 4 — Pick the widget. Which Consumer variant do you need if the screen must also own a TextEditingController?

Show solution

ConsumerStatefulWidget (with ConsumerState) — it gives you both a ref and the normal State lifecycle (initState/dispose) to create and dispose the controller.


Questions to test yourself

Q1 (basic). What does a plain Provider expose, and when does its create function run?

Show answer

A read-only value. Its create function runs lazily the first time the provider is read, and the result is cached (recomputed only if a dependency changes or it's invalidated).

Q2 (basic). How is ConsumerWidget different from StatelessWidget?

Show answer

It's the same, except its build method receives an extra WidgetRef ref parameter, which you use to read providers (e.g. ref.watch(...)). Use it instead of StatelessWidget when the widget needs providers.

Q3 (intermediate). What does ref.watch(provider) do in a build method?

Show answer

It reads the provider's current value and subscribes to it, so the widget rebuilds automatically whenever that provider's value changes. It's the standard way to display reactive state.

Q4 (intermediate). When would you use Consumer instead of making the whole widget a ConsumerWidget?

Show answer

To narrow the rebuild scope — wrap only the part of the UI that depends on a provider in a Consumer, so just that part rebuilds when the provider changes while expensive sibling widgets stay static. It's a performance optimization.

Q5 (intermediate). How does one provider depend on another, and what happens when the dependency changes?

Show answer

Inside the provider's create function, call ref.watch(otherProvider). This makes the provider depend on the other; when the other's value changes, this provider recomputes, and any widget watching it rebuilds — Riverpod's reactive graph at work.

Q6 (advanced). Why is plain Provider ideal for dependency injection, and how does that interact with testing?

Show answer

Provider exposes a single, cached, shared value — perfect for a service/repository instance that the rest of the app depends on via ref.watch instead of constructing it everywhere. Because it's just a provider, you can override it in a ProviderScope/ProviderContainer (Part 3) to inject a fake in tests — so production code depends on the real implementation and tests transparently get the mock, with no code changes in the widgets.


Wrapping up

You've written the core Riverpod loop:

  • Provider<T> declares a cached, read-only value via a (ref) => ... function — great for derived values and dependency injection (services/repositories).
  • ConsumerWidget is a StatelessWidget with a ref; ref.watch(provider) reads and subscribes, rebuilding on change.
  • Use Consumer to narrow rebuilds and ConsumerStatefulWidget when you also need State (controllers, initState).
  • Providers depend on other providers via ref.watch inside their create function, forming a reactive graph.

We've used ref.watch throughout — but ref has three reading methods, and choosing the wrong one is the most common Riverpod mistake. Part 5, the Foundation finale before the question bank, nails down ref.watch vs ref.read vs ref.listen — when to use what.