Migrating a production Flutter app from Provider to Riverpod cut our state-related bug reports by 60% and made the codebase testable in ways that weren’t possible before — here’s the step-by-step path we took and the open-source repository you can fork today.
The app in question was a logistics management tool used by a Costa Rican distribution company to coordinate 200+ daily deliveries across the Greater Metropolitan Area. Built eighteen months earlier with Provider, the codebase had grown to 140 screens and roughly 45 Provider instances scattered across the widget tree. The team was hitting classic Provider pain points: ChangeNotifier classes ballooning to 400+ lines, circular dependency warnings during hot reload, and an inability to unit-test business logic without spinning up a full widget tree. When a critical bug — delivery status updates silently failing because a disposed ChangeNotifier was still receiving stream events — cost the client a full day of operational visibility, they approved the migration.
We approached the migration incrementally rather than as a big-bang rewrite. Phase one involved wrapping the existing MaterialApp in a ProviderScope and converting the six most critical state holders — authentication, active delivery tracking, driver location streaming, notification preferences, offline queue, and the route optimization cache — from ChangeNotifierProvider to Riverpod’s StateNotifierProvider. Each conversion followed the same pattern: extract business logic from the ChangeNotifier into a pure Dart StateNotifier class, define the state as a freezed union type for exhaustive pattern matching, and replace all context.read/context.watch calls in the widget tree with ConsumerWidget and ref.watch. This phase took one sprint and immediately resolved the disposed-listener bug because Riverpod’s autoDispose modifier handles lifecycle management declaratively.
Phase two tackled the remaining 39 providers over three sprints, prioritizing by test coverage impact. We introduced AsyncNotifierProvider for all network-dependent state — API calls, Firestore streams, local database reads — which gave us built-in loading, error, and data states without writing custom enum wrappers. The team adopted a convention of one provider per feature file, co-located with the feature’s widgets and tests, which made the dependency graph explicit and navigable. By the end of phase two, unit test coverage on business logic had jumped from 22% to 71%, and the integration test suite ran 35% faster because Riverpod’s ProviderContainer allowed us to override dependencies without mocking entire widget subtrees.
We open-sourced the migration scaffold as a template repository on GitHub under the Gianko organization. It includes the freezed state models, the StateNotifier base patterns, a CI configuration for running Riverpod-lint rules on every pull request, and a detailed README walking through each conversion step. The repository has been used internally on two subsequent Flutter projects and has received contributions from developers outside the team. For any Flutter team still running Provider at scale, the migration is less painful than it appears — the key is converting incrementally, starting with the state holders that cause the most bugs, and letting Riverpod’s compile-time safety catch the dependency errors that Provider only surfaces at runtime.



