DEV Community

Cover image for 【Flutter / Riverpod】container.read no mantiene vivo tu provider: la trampa de autoDispose en la prueba.
Aoi Daniel Kaneda
Aoi Daniel Kaneda

Posted on Edited on Originally published at Medium

【Flutter / Riverpod】container.read no mantiene vivo tu provider: la trampa de autoDispose en la prueba.

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

※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;
}
Enter fullscreen mode Exit fullscreen mode

🔍 ¿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:

  1. Se crea el container con ProviderContainer.test().
  2. await container.read(testProvider.future) crea el provider dentro del container y se ejecuta build.
  3. Como el provider no tiene suscriptores, se elimina automáticamente durante el await.
  4. container.read(testProvider) crea el provider otra vez.
  5. Se llama a requireValue antes de que termine build, así que el AsyncValue todavía no tiene valor.
  6. 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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

🏁 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)

Collapse
 
stefan_07d23f966225156b50 profile image
Stefan •

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: true later just to make a failing test pass.

Collapse
 
aoi_kaneda_7cc8c7176fe43b profile image
Aoi Daniel Kaneda • • Edited

Thank you so much for reading and adding your comment!
I completely agree with you. If someone adds keepAlive: true to make a test pass, a disposal test would act as guard.

I’ll add it!!