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 aString.(ref) { return ... }— the create function. It runs lazily the first time the provider is read, and its result is cached. Thereflets 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:
ConsumerWidgetreplacesStatelessWidgetwhen you need to read providers. Same idea, plus aref.ref.watch(provider)reads the provider's current value and subscribes to it — so when the provider's value changes, this widget rebuilds automatically. (Fullwatch/read/listenbreakdown 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.watchinside a provider's create function to depend on another provider. Don't useref.readthere — 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).ConsumerWidgetis aStatelessWidgetwith aref;ref.watch(provider)reads and subscribes, rebuilding on change.- Use
Consumerto narrow rebuilds andConsumerStatefulWidgetwhen you also needState(controllers,initState). - Providers depend on other providers via
ref.watchinside 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.