Migrar de provider a Riverpod en Flutter (de provider 6.1.5 a Riverpod 3.x)
Una migracion paso a paso del paquete provider a Riverpod 3.x en una app de Flutter real: de ChangeNotifierProvider a Notifier, de MultiProvider a ProviderScope, de context.watch a ref.watch, de ProxyProvider a composicion con ref.watch, mas los detalles de igualdad y ciclo de vida que muerden. Probado en Flutter 3.27.1, Dart 3.11, provider 6.1.5, flutter_riverpod 3.3.1.
La version corta: agrega flutter_riverpod junto a provider, envuelve tu app en un ProviderScope en vez de un MultiProvider, y migra una caracteristica a la vez empezando por las hojas de tu arbol de dependencias. Cada ChangeNotifier se convierte en un Notifier (o AsyncNotifier para trabajo asincrono), context.watch<T>() se convierte en ref.watch(myProvider), Provider.of y context.read se convierten en ref.read, y cada ProxyProvider colapsa en un simple ref.watch de otro provider. Una app pequena o mediana es un trabajo de uno a tres dias; la parte que rompe no es la sintaxis, es que Riverpod compara el estado por igualdad y mantiene los providers vivos de forma distinta a como lo hace provider. Probado en Flutter 3.27.1, Dart 3.11, provider 6.1.5, flutter_riverpod 3.3.1, riverpod_annotation 2.6.1 y riverpod_generator 2.6.5.
El paquete provider (actualmente 6.1.5) ha sido la respuesta por defecto para el manejo de estado en Flutter desde 2019, y sigue funcionando. Pero su autor, Remi Rousselet, escribio Riverpod especificamente para arreglar los problemas estructurales de provider: leer el estado a traves de BuildContext significa un ProviderNotFoundException en tiempo de ejecucion en vez de un error de compilacion, el anidamiento de ProxyProvider se vuelve ilegible pasadas dos dependencias, y no puedes tener dos providers del mismo tipo sin malabares con ValueKey. Riverpod conserva el modelo mental (un grafo de objetos que reconstruyen widgets cuando cambian) y elimina el acoplamiento con BuildContext. Esta guia es la migracion mecanica, primero las hojas, que no requiere reescribir todo.
Por que migrar fuera de provider
- Seguridad en tiempo de compilacion en vez de
ProviderNotFoundException. Enprovider, leer un tipo que no esta por encima de ti en el arbol lanza una excepcion en tiempo de ejecucion. En Riverpod, los providers son globales de nivel superior, asi que un error de tipeo es un error de compilacion y no hay nada que “encontrar”. - Se acabo la piramide de
MultiProvider. Riverpod no tiene un arbol de providers que ensamblar. Un soloProviderScopeen la raiz reemplaza toda la listaMultiProvider(providers: [...]), y las dependencias entre providers se expresan conref.watch, no con el orden de anidamiento. - Dos providers del mismo tipo, gratis.
providerindexa todo por tipo, asi que dosChangeNotifierProvider<CartModel>colisionan. Riverpod indexa por el objeto provider, asi que esto no es un problema. - Auto-dispose y family que de verdad componen. Riverpod te da providers
autoDisposey parametrizados (family) como caracteristicas de primera clase, algo queprovidersolo aproxima conChangeNotifierProvider.valuemanual y manejo de keys.
Que rompe
| Area | Cambio | Severidad |
|---|---|---|
| Cableado raiz | MultiProvider reemplazado por un solo ProviderScope | media |
| Lecturas | context.watch<T>() / Provider.of<T>(context) reemplazado por ref.watch / ref.read | alta |
| Notifiers | ChangeNotifier + notifyListeners() reemplazado por Notifier + reasignacion de estado | alta |
| Semantica de reconstruccion | Riverpod compara el estado por ==; la mutacion en el lugar ya no reconstruye | alta |
| Composicion | ProxyProvider reemplazado por ref.watch de la dependencia | media |
| Widgets | StatelessWidget / StatefulWidget se vuelven ConsumerWidget / ConsumerStatefulWidget | media |
| Ciclo de vida | provider libera cuando se quita del arbol; Riverpod mantiene el estado hasta autoDispose | media |
Las dos filas alta en las areas de reconstruccion y notifier son donde los equipos pierden tiempo. Todo lo demas es buscar y reemplazar.
Lista de verificacion previa
- Flutter 3.27.1 / Dart 3.11 (o mas nuevo) instalado:
flutter --version. - Un arbol de trabajo
gitlimpio y una rama que puedas desechar. - Un inventario de cada provider que registras hoy. Haz grep en tu codigo:
grep -rn "ChangeNotifierProvider\|ProxyProvider\|FutureProvider\|StreamProvider\|Provider.of\|context.watch\|context.read" lib/. - Una nota junto a cada uno sobre si algo depende de el. Migra primero las cosas de las que nada depende.
- Un conjunto de pruebas que funcione, aunque sea minimo. Lo ejecutaras despues de cada paso.
Pasos de migracion
-
Agrega Riverpod junto a provider en
pubspec.yaml. No quitesprovidertodavia. Ambos paquetes coexisten porque poseen arboles separados; una pieza de estado tiene exactamente un dueno a la vez, asi que migra por caracteristica, no por tipo.# pubspec.yaml. Flutter 3.27.1, Dart 3.11. dependencies: flutter: sdk: flutter provider: ^6.1.5 # keep until migration is done flutter_riverpod: ^3.3.1 riverpod_annotation: ^2.6.1 dev_dependencies: build_runner: ^2.4.13 riverpod_generator: ^2.6.5 custom_lint: ^0.7.0 riverpod_lint: ^2.6.5Verifica:
flutter pub getresuelve sin conflictos de version. -
Envuelve la raiz de la app en
ProviderScopey mantenMultiProviderdentro de el por ahora.ProviderScopees donde Riverpod almacena todo el estado de los providers. No es una lista de providers, es un solo limite. Deja tuMultiProviderexistente debajo de el para que las pantallas no migradas sigan funcionando.// lib/main.dart, Flutter 3.27.1 import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:provider/provider.dart'; void main() { runApp( ProviderScope( // Riverpod root child: MultiProvider( // legacy, shrinks as you migrate providers: [ ChangeNotifierProvider(create: (_) => CartModel()), ChangeNotifierProvider(create: (_) => AuthModel()), ], child: const MyApp(), ), ), ); }Verifica: la app sigue compilando y ejecutandose identicamente. Nada se ha movido todavia.
-
Convierte un
ChangeNotifierhoja en unNotifier. Elige un modelo del que nada mas dependa. Enprovidermutas un campo y llamas anotifyListeners(). En Riverpod,build()devuelve el estado inicial y reasignasstatepara notificar. No haynotifyListeners().// Before: provider 6.1.5 class CartModel extends ChangeNotifier { final List<Item> _items = []; List<Item> get items => List.unmodifiable(_items); void add(Item item) { _items.add(item); notifyListeners(); } }// After: flutter_riverpod 3.3.1, code generation import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'cart_model.g.dart'; @riverpod class Cart extends _$Cart { @override List<Item> build() => const []; void add(Item item) { state = [...state, item]; // new list, not state.add(...) } }Ejecuta
dart run build_runner build --delete-conflicting-outputspara generarcartProvider. Verifica: el generador producecart_model.g.dartsin errores. -
Cambia la pantalla que lo consume a un
ConsumerWidget.StatelessWidgetse vuelveConsumerWidgetybuildgana unWidgetRef ref.context.watch<CartModel>()se convierte enref.watch(cartProvider). Para una llamada a un metodo,context.read<CartModel>().add(x)se convierte enref.read(cartProvider.notifier).add(x).// Before class CartView extends StatelessWidget { @override Widget build(BuildContext context) { final items = context.watch<CartModel>().items; return Column(children: [ for (final i in items) Text(i.name), ElevatedButton( onPressed: () => context.read<CartModel>().add(Item('pen')), child: const Text('Add'), ), ]); } }// After class CartView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final items = ref.watch(cartProvider); return Column(children: [ for (final i in items) Text(i.name), ElevatedButton( onPressed: () => ref.read(cartProvider.notifier).add(Item('pen')), child: const Text('Add'), ), ]); } }Si el widget ya tenia su propio estado, usa
ConsumerStatefulWidgetmasConsumerState, donderefesta disponible como un campo. Quita la lineaChangeNotifierProvider(create: (_) => CartModel())deMultiProvider. Verifica: la pantalla se comporta igual y la lista deMultiProvideres una entrada mas corta. -
Reemplaza
ProxyProvidercon composicionref.watch. Este es el paso que borra mas codigo. UnProxyProviderque construye B a partir de A se convierte en un provider que simplemente observa A.// Before: ProxyProvider wiring ProxyProvider<AuthModel, ApiClient>( update: (_, auth, __) => ApiClient(token: auth.token), ),// After: a provider that watches its dependency @riverpod ApiClient apiClient(ApiClientRef ref) { final token = ref.watch(authProvider.select((a) => a.token)); return ApiClient(token: token); }ref.watch(...select(...))es el reemplazo directo decontext.selectdeprovider, y significa queapiClientsolo se reconstruye cuando cambiatoken, no en cada actualizacion deAuthModel. Verifica: los widgets dependientes se reconstruyen cuando cambia el provider de arriba. -
Migra
FutureProvideryStreamProvidera sus equivalentes en Riverpod. Los nombres son los mismos; solo difiere el cableado. UnFutureProviderdeproviderse lee con tuberia al estilocontext.watch<AsyncSnapshot>; el de Riverpod devuelve unAsyncValue<T>sobre el que haces switch directamente.// After: flutter_riverpod 3.3.1 @riverpod Future<User> currentUser(CurrentUserRef ref) { return ref.watch(apiClientProvider).fetchUser(); } // in a ConsumerWidget final userAsync = ref.watch(currentUserProvider); return userAsync.when( data: (user) => Text(user.name), loading: () => const CircularProgressIndicator(), error: (e, _) => Text('Failed: $e'), );Verifica: los estados de carga y error se renderizan sin banderas manuales
bool isLoading. Para mas sobre este patron, mira el articulo enlazado sobre AsyncValue mas abajo. -
Elimina la dependencia de
provider. Una vez queMultiProvidereste vacio, quitalo demain.dart, luego eliminaprovider: ^6.1.5depubspec.yamly ejecutaflutter pub get. El compilador marcara cualquier llamada restante acontext.watch/context.read/Provider.of. Verifica: el proyecto compila con cero referencias apackage:provider.
Verificacion
Ejecuta esta lista de verificacion despues del ultimo paso, no solo al final:
flutter analyzeno reporta errores ni advertencias deriverpod_lint.dart run build_runner build --delete-conflicting-outputstermina limpio.flutter testpasa. Las pruebas de Riverpod usanProviderContainer(oProviderContainer.test()en 3.x) ycontainer.read(provider), reemplazando tus viejas pruebas unitarias deChangeNotifier.- Una pasada manual de humo: cada pantalla migrada sigue reconstruyendose al cambiar el estado, y ninguna pantalla lanza
ProviderNotFoundException(no deberia quedar ninguna, por construccion). grep -rn "package:provider" lib/no devuelve nada.
Plan de reversion
Esta migracion es reversible por caracteristica precisamente porque ambos paquetes coexisten. Si una pantalla migrada se comporta mal, revierte el commit de esa pantalla: vuelve a poner la linea ChangeNotifierProvider en MultiProvider, restaura la clase ChangeNotifier, y cambia el widget de vuelta a StatelessWidget. Como migraste primero las hojas y una caracteristica por commit, ninguna reversion toca mas de una pantalla. No elimines provider de pubspec.yaml (paso 7) hasta que tengas confianza, ya que esa es la unica puerta de un solo sentido en la secuencia.
Detalles con los que tropezamos
La mutacion en el lugar deja de reconstruir. Esta es la sorpresa numero uno. En provider, _items.add(x); notifyListeners() funciona porque tu controlas la notificacion. En un Notifier de Riverpod, el framework reconstruye solo cuando a state se le asigna un valor que no es == al anterior. state.add(x) muta la misma lista, la referencia no cambia, y nada se reconstruye. Siempre asigna una nueva coleccion: state = [...state, x]. Lo mismo aplica a los objetos de modelo, por lo cual el estado inmutable (records, copyWith, o una clase freezed) se empareja naturalmente con Riverpod.
Los providers no se liberan cuando el widget sale del arbol. Un ChangeNotifierProvider de provider se libera cuando se quita su subarbol. Un provider de Riverpod, por defecto, mantiene su estado durante toda la vida del ProviderScope. Si dependias de que el controlador de una pantalla se reiniciara al salir de ella, ahora necesitas autoDispose (o, con generacion de codigo, ese es el comportamiento por defecto para providers anotados a menos que llames a ref.keepAlive()). Audita cualquier provider cuyo comportamiento antiguo dependia de la liberacion basada en el arbol.
ref.read dentro de build() es una trampa. Leer otro provider con ref.read dentro de un Notifier.build() o el build de un widget toma una instantanea del valor una sola vez y nunca se actualiza. Usa ref.watch para cualquier cosa que deba reaccionar, y reserva ref.read para manejadores de eventos como callbacks de botones. riverpod_lint marca la mayoria de estos por ti, por lo cual vale la pena instalar la dependencia de desarrollo desde el primer dia.
Consumer existe en ambos paquetes. Si importas ambos durante la migracion, Consumer es ambiguo. El Consumer de Riverpod toma un builder (context, ref, child); el de provider toma (context, value, child). Prefiere convertir el widget completo a ConsumerWidget en vez de meter un Consumer de Riverpod en un widget de la era de provider, y asi evitaras por completo el choque de imports.
La migracion premia ser aburrido: una hoja, un commit, ejecuta las pruebas, repite. Para cuando elimines provider de pubspec.yaml, la parte riesgosa ya paso hace semanas en pasos pequenos y reversibles.
Relacionados
- Si vienes de una biblioteca de estado distinta, la migracion de GetX a Riverpod cubre el mismo enfoque de primero las hojas para un framework mas pesado.
- Aun lo estas decidiendo? La comparacion de provider vs Riverpod vs Bloc expone las concesiones antes de que te comprometas.
- Para el lado asincrono, estados de carga y error con AsyncValue profundiza en
whenyAsyncNotifier. - Vienes de
FutureBuilderpuro? Mira FutureBuilder/StreamBuilder vs Riverpod AsyncValue. - El error en tiempo de ejecucion mas comun despues de migrar: Cannot use ref after the widget was disposed.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.