El autoDispose de Riverpod elimina automáticamente el estado de los providers que ya no tienen suscriptores. Es un mecanismo muy útil: ahorra memoria y evita errores causados por estados que quedan en la app sin querer.
Sin embargo, si no lo tenemos en cuenta al escribir pruebas, puede pasar que fallen aunque el código de producción sea correcto.
💻 Entorno de ejecución
- Flutter 3.47.2
- flutter_riverpod ^3.4.2
- riverpod_annotation ^4.0.6
- riverpod_generator ^4.0.8
📄 Código de ejemplo
Supongamos que preparamos testProvider y queremos verificar su comportamiento manteniendo TestState en una misma prueba. Sin embargo, con este código ambas pruebas fallan.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
Future<ProviderContainer> makeContainer() async {
final container = ProviderContainer.test();
await container.read(testProvider.future);
return container;
}
void main(){
test("inicialización", () async {
final container = await makeContainer();
final state = container.read(testProvider).requireValue;
expect(state.count, 0);
});
test("se incrementa uno", () async {
final container = await makeContainer();
await container.read(testProvider.notifier).increment();
final state = container.read(testProvider).requireValue;
expect(state.count, 1);
});
}
※TestNotifier / TestState
Agregamos Future.delayed en build para simular la llamada a una API o a la base de datos.
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter/foundation.dart';
part 'test_notifier.g.dart';
@riverpod
class TestNotifier extends _$TestNotifier {
@override
Future<TestState> build() async {
await Future.delayed(const Duration(milliseconds: 5));
return const TestState(count: 0);
}
Future<void> increment() async {
final current = await future;
state = AsyncData(TestState(count: current.count + 1));
}
}
@immutable
class TestState {
const TestState({required this.count});
final int count;
}
🔍 ¿Por qué sucedió?
La causa es usar container.read para leer el estado del provider. container.read solo lee el valor actual y no se suscribe a sus cambios. Por eso, un provider con autoDispose queda sin suscriptores y se elimina automáticamente.
Hay que tener cuidado con los providers generados con @riverpod, porque tienen autoDispose activado por defecto.
Por ejemplo, la prueba de inicialización falla siguiendo este flujo:
- Se crea el container con
ProviderContainer.test(). -
await container.read(testProvider.future)crea el provider dentro del container y se ejecutabuild. - Como el provider no tiene suscriptores, se elimina automáticamente durante el
await. -
container.read(testProvider)crea el provider otra vez. - Se llama a
requireValueantes de que terminebuild, así que elAsyncValuetodavía no tiene valor. - Se lanza una excepción y la prueba falla.
Mensaje de error
AsyncValueIsLoadingException: requireValue was called on the async value AsyncLoading TestState(), yet it neither has an error nor a value.
✅ Soluciones
Hay dos soluciones. Elige la que mejor se adapte a los requisitos de tu app.
1. Escuchar el provider con container.listen()
Este método consiste en suscribirse al provider mediante container.listen. No importa que el callback esté vacío: mientras la suscripción se mantenga, el provider no se elimina y su estado se conserve durante toda la prueba.
Future<ProviderContainer> makeContainer() async {
final container = ProviderContainer.test();
container.listen(testProvider, (_, _){});
await container.read(testProvider.future);
return container;
}
La documentación oficial de Riverpod también recomienda este método.
Be careful when using container.read when providers are automatically disposed.
If your provider is not listened to, chances are that its state will get destroyed in the middle of your test.
In that case, consider using container.listen.
Testing your providers | Riverpod
2. Dejar de usar autoDispose
La otra opción es desactivar autoDispose para que el estado del provider se mantenga aunque no tenga suscripciones.
@Riverpod(keepAlive: true) // <- cambiar keepAlive a true y regenerar
class TestNotifier extends _$TestNotifier {
// igual que antes
}
Sin embargo, con este método el estado del provider también se mantiene en producción, lo que puede causar errores inesperados. Hay que llamar a ref.invalidate cuando se necesita reiniciar el estado, por ejemplo, al navegar a otra pantalla o al cerrar sesión.
Si mantienes autoDispose(Opción 1), conviene agregar una prueba que verifique que el estado se elimina al cerrar la suscripción. Así, si alguien añade keepAlive: true solo para que otra prueba pase, esta prueba fallará y lo detectará.
test("Después de cerrar la suscripción, se elimina su estado.", () async {
final container = ProviderContainer.test();
final sub = container.listen(testProvider, (_, _){});
await container.read(testProvider.future);
expect(container.exists(testProvider), isTrue);
sub.close();
await container.pump();
expect(container.exists(testProvider), isFalse); // Si Provider tiene keepAlive, falla
});
🏁 Conclusión
En este artículo presenté una trampa oculta de autoDispose al escribir pruebas. Para trabajar con tranquilidad, lo recomendable es usar container.listen como base en las pruebas.
Poder escribir rápidamente pruebas robustas también influye directamente en la calidad del código de producción.
Top comments (2)
I'd add a separate test that closes the subscription and checks that disposal happens too. Keeping the provider alive makes sense for the increment test, but I'd still want a test that catches someone adding
keepAlive: truelater just to make a failing test pass.Thank you so much for reading and adding your comment!
I completely agree with you. If someone adds
keepAlive: trueto make a test pass, a disposal test would act as guard.I’ll add it!!