S

super-dev — full-stack

@super-dev.app · Full-stack · ★ disponible para proyectos
12,8k suscriptores·47 vídeos·Activo desde 2017

Desarrollador full-stack .NET / Angular con perfil DevOps en Azure Cloud. También cómodo con Flutter + Firebase. Construyo productos, los despliego, los monitorizo.

Volver a los artículos
FLUTTER
FLUTTER

Una arquitectura Flutter testable con Riverpod

Artículo 2 de 3 — Flutter en producción
Offline-first, Riverpod, monorepo Melos: mobile en serio, no un prototipo.

Una app Flutter que crece siempre acaba planteando la misma pregunta: ¿dónde vive el estado y cómo se prueba sin arrancar un widget? setState mezcla lógica e interfaz en la misma clase; InheritedWidget propaga bien el dato pero no dice nada de su creación ni de su reemplazo en pruebas. Riverpod responde a ambas: un contenedor de inyección de dependencias que produce valores reactivos, independientes del árbol de widgets.

Providers y notifiers

Un Provider expone un valor; un Notifier expone un valor mutable acompañado de la lógica que lo hace evolucionar. Con la generación de código ( riverpod_generator ), se anota una clase y el build() devuelve el estado inicial:

Dart
1@riverpod
2class Counter extends _$Counter {
3 @override
4 int build() => 0;
5
6 void increment() => state = state + 1;
7}
8
9@riverpod
10Future<User> currentUser(CurrentUserRef ref) {
11 final api = ref.watch(apiClientProvider);
12
13 return api.fetchMe();
14}

El ref.watch dentro de un provider crea una dependencia : si apiClientProvider cambia, currentUser se recalcula automáticamente. Es el grafo de dependencias lo que reemplaza los setState manuales en cascada.

Separar la interfaz de la lógica

La regla de oro: un widget no contiene ninguna lógica de negocio. Lee el estado y llama a métodos. Toda la mecánica vive en el notifier, testeable sin WidgetTester :

Dart
1class CounterView extends ConsumerWidget {
2 const CounterView({super.key});
3
4 @override
5 Widget build(BuildContext context, WidgetRef ref) {
6 final count = ref.watch(counterProvider);
7
8 return Text('$count');
9 }
10}

En cuanto a la lectura asíncrona, un ref.watch(currentUserProvider) devuelve un AsyncValue , cuyo .when(data:, loading:, error:) cubre los tres estados sin un booleano `isLoading` vagabundo .

Inyección de dependencias y overrides en pruebas

Aquí es donde Riverpod supera a InheritedWidget : cada provider es reemplazable al montar el ProviderContainer . En pruebas, se inyecta un cliente API falso sin tocar el código de producción:

Dart
1test('charge l_utilisateur courant', () async {
2 final container = ProviderContainer(
3 overrides: [
4 apiClientProvider.overrideWithValue(FakeApiClient()),
5 ],
6 );
7 addTearDown(container.dispose);
8
9 final user = await container.read(currentUserProvider.future);
10 expect(user.name, 'Ada');
11});

Sin mock global, sin singleton que reinicializar entre pruebas: cada container está aislado, y addTearDown garantiza que se libere. La documentación de pruebas de Riverpod detalla los patrones de pump y de listeners.

Por qué no setState ni InheritedWidget

setState reconstruye todo el State y mantiene la lógica soldada a la interfaz — imposible de probar sin renderizar el widget. InheritedWidget comparte un valor pero obliga a escribir manualmente el updateShouldNotify , no gestiona ni la asincronía ni el reemplazo, y se escapa en cuanto se toca el BuildContext . Riverpod desplaza el estado fuera del árbol , lo hace perezoso, memoizado y auto-eliminado ( autoDispose ), y convierte el override en la vía normal de prueba.

Una buena arquitectura Flutter no se mide por el número de providers, sino por esto: ¿se puede probar la lógica sin montar nunca un widget ? Con Riverpod, la respuesta es sí por construcción.
super-dev — portfolio.app
// Continuar en FLUTTER
Sync offline-first en Flutter + Firebase
9 min • 2,2k lecturas
Monorepo Flutter con Melos
6 min • 1,2k lecturas