1. Providers Without BuildContext
flutter pub add flutter_riverpod
void main() {
runApp(const ProviderScope(child: MyApp()));
}
final greetingProvider = Provider<String>((ref) => 'Hello, Flutter!');
class GreetingText extends ConsumerWidget {
const GreetingText({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Text(greeting);
}
}
Providers are defined as top-level, global variables — but they're not global
mutable state in the dangerous sense; ProviderScope gives each one a
real, overridable, testable instance, which is precisely what removing the
BuildContext dependency makes possible.
2. Modeling Mutable State with Notifier
class CartNotifier extends Notifier<List<String>> {
@override
List<String> build() => []; // initial state
void add(String item) {
state = [...state, item]; // reassigning state triggers listeners
}
void remove(String item) {
state = state.where((i) => i != item).toList();
}
}
final cartProvider = NotifierProvider<CartNotifier, List<String>>(CartNotifier.new);
class CartBadge extends ConsumerWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartProvider);
return Badge(label: Text('${items.length}'));
}
}
// elsewhere, mutating:
ref.read(cartProvider.notifier).add('Widget');
Reassigning state (rather than mutating it in place, as
[...state, item] does above) is deliberate — Riverpod compares the old
and new state to decide whether to notify listeners, which requires a genuinely new
value, not an in-place mutation of the same list.
3. Compile-Time Safety & Testing
Provider's context.watch<CartModel>() fails at runtime, with a
thrown exception, if no matching provider exists above that widget in the tree.
Riverpod's providers are strongly typed top-level references — a typo or a missing
provider is caught by the analyzer, before the app ever runs.
test('adding an item increases cart length', () {
final container = ProviderContainer();
addTearDown(container.dispose);
container.read(cartProvider.notifier).add('Widget');
expect(container.read(cartProvider).length, 1);
});
ProviderContainer gives direct access to provider state with no
BuildContext and no widget tree at all — a real, meaningful advantage
for testing business logic in isolation, ahead of Week 16's testing work, without
needing WidgetTester just to verify a notifier's behavior.
4. Provider vs. Riverpod vs. Bloc
- Provider — a solid choice for a smaller app, or a team already deeply familiar with it; simplest mental model, tied to
BuildContext. - Riverpod — the default recommendation going forward in this course: compile-time safety, easy testing, no context dependency, and it's what the rest of this curriculum builds on.
- Bloc — worth reaching for when a team wants a strict, explicit separation between events (inputs) and states (outputs), often for larger teams or apps with complex, auditable business logic where that ceremony pays for itself.
All three solve the same underlying problem from Week 6 — none is "more correct" in the abstract; the right choice depends on team size, how much explicit structure the app's state logic actually needs, and (realistically) what a team already knows.
5. Hands-on Exercise
Migrate the cart app to Riverpod, with a real unit test
Rebuild Week 7's Provider-based cart on Riverpod, and test the notifier without any widgets.
Requirements:
- A
CartNotifier(Notifier<List<String>>) withaddandremove, wrapped inNotifierProvider. - Wrap the app in
ProviderScopeand rebuild the cart badge and product screen asConsumerWidgets usingref.watch/ref.read. - A unit test using
ProviderContainerthat adds and removes items and asserts the final cart contents, with no widget tree involved at all. - Deliberately reference a provider that doesn't exist (a typo'd name) and confirm the analyzer flags it before you even run the app — contrast this with Provider's runtime failure mode from this week's third section.
- A short comment comparing your experience testing the Riverpod version against how you'd have to test the equivalent Provider-based
ChangeNotifier(which needs a widget tree, or at least a mocked context, to exercise realistically).
If state = ... inside your notifier doesn't seem to trigger a rebuild anywhere watching it, check that you're assigning a genuinely new value (a new list via spread, [...state, item]) rather than mutating the existing list in place (state.add(item)) — Riverpod's default equality check won't detect an in-place mutation as a change.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why can Riverpod providers be defined as top-level variables without that being dangerous global mutable state?
Why can Riverpod providers be defined as top-level variables without that being dangerous global mutable state?
ProviderScope gives each provider a real, scoped instance per app (or per test, via ProviderContainer) rather than the top-level declaration itself holding the actual mutable data — the top-level variable is just a typed reference/key used to look up that scoped instance, which is also what makes providers overridable in tests without touching global state.
Q2
What class of bug does Riverpod's compile-time safety catch that Provider's context.watch<T>() cannot, and when does each actually surface?
What class of bug does Riverpod's compile-time safety catch that Provider's context.watch<T>() cannot, and when does each actually surface?
A missing or mistyped provider reference — Riverpod's typed top-level provider objects mean the analyzer catches a reference to a provider that doesn't exist (or a type mismatch) before the app ever runs. Provider's context-based lookup can't be checked until runtime, so the identical class of mistake only surfaces as a thrown exception when that specific widget actually tries to build.
Q3
Why does ProviderContainer make testing a notifier's logic meaningfully easier than testing the equivalent Provider-based ChangeNotifier?
Why does ProviderContainer make testing a notifier's logic meaningfully easier than testing the equivalent Provider-based ChangeNotifier?
It gives direct read/write access to provider state with no BuildContext and no widget tree required at all — a plain unit test can exercise the notifier's methods and assert on its state directly. Testing a context-dependent ChangeNotifier realistically requires at least a minimal widget tree (or a mocked context) just to reach it, which is more setup for the same underlying test.
Q4
Per this week's framework, what would specifically justify choosing Bloc over Riverpod for a project, rather than defaulting to Riverpod?
Per this week's framework, what would specifically justify choosing Bloc over Riverpod for a project, rather than defaulting to Riverpod?
A team or project that wants a strict, explicit, auditable separation between events (what happened) and states (what the UI should show) — Bloc enforces that structure directly, which is valuable for larger teams or complex business logic where that ceremony and traceability pays for itself. For most other cases, Riverpod's lighter structure is the better default, per this week's recommendation.