Gestión Eficiente del Estado en Flutter con Riverpod.

Introducción a Riverpod

Riverpod es una biblioteca de gestión de estado para Flutter. Es una alternativa a otras bibliotecas populares como Provider, que proporciona una manera simple y eficiente de administrar y compartir el estado entre los widgets en tu aplicación.

Riverpod está diseñado para ser fácil de usar y entender, y se basa en la sintaxis de Dart moderna para proporcionar una experiencia de desarrollo fluida. En este artículo, vamos a explorar las características clave de Riverpod y cómo puedes utilizarlo en tu aplicación Flutter.

¿Qué es Riverpod?

Riverpod es una biblioteca de gestión de estado para Flutter que se basa en la sintaxis moderna de Dart. Es una alternativa a Provider, otra biblioteca popular de gestión de estado en Flutter.

En lugar de utilizar clases de cambio de notificación, Riverpod utiliza una combinación de constructores y funciones de fábrica para proporcionar un enfoque más moderno y fácil de entender para la gestión de estado.

Riverpod se basa en el concepto de «proveedores», que son objetos que pueden ser compartidos por varios widgets en tu aplicación. Los proveedores pueden ser instancias de una clase, funciones, o cualquier otro objeto que desees compartir.

Para acceder a un proveedor en tu widget, simplemente utilizas el widget Consumer o el método ref.watch para observar cambios en el proveedor. Si el estado del proveedor cambia, el widget se reconstruye automáticamente.

Configuración de Riverpod

Para utilizar Riverpod en tu aplicación Flutter, debes agregarlo como una dependencia en tu archivo pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  riverpod: ^1.0.3

Una vez que hayas agregado Riverpod a tu archivo pubspec.yaml, debes importarlo en tus archivos Dart donde desees utilizarlo:

import 'package:flutter_riverpod/flutter_riverpod.dart';

Proveedores (Providers)

En Riverpod, los proveedores son objetos que pueden ser compartidos por varios widgets en tu aplicación. Los proveedores pueden ser instancias de una clase, funciones, o cualquier otro objeto que desees compartir.

Para crear un proveedor, utilizas el método Provider o StateNotifierProvider, que es una variante de Provider diseñada para trabajar con el patrón de diseño de Notificador de Estado.

Proveedor básico

El siguiente ejemplo muestra cómo crear un proveedor básico que devuelve una cadena:

final greetingProvider = Provider((_) => 'Hello, world!');

En este ejemplo, greetingProvider es un proveedor que devuelve una cadena de saludo. Para acceder a esta cadena en un widget, utilizas el widget Consumer o el método ref.watch:

class GreetingWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final greeting = ref.watch(greetingProvider);
    return Text(greeting);
  }
}

En este ejemplo, GreetingWidget es un widget que utiliza ConsumerWidget para acceder al proveedor greetingProvider y mostrar el saludo en un widget Text.

Proveedor de estado (State Provider)

En Riverpod, también puedes crear proveedores de estado utilizando el método StateNotifierProvider. Estos proveedores son ideales para trabajar con el patrón de Notificador de Estado, que es un patrón de diseño común en Flutter para actualizar el estado de un widget.

Para crear un proveedor de estado, primero debes crear una clase que extienda StateNotifier y que contenga la lógica para actualizar el estado del proveedor. Por ejemplo, aquí hay una clase Counter que mantiene el estado de un contador:

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

En este ejemplo, la clase Counter extiende StateNotifier<int>, lo que indica que el estado del proveedor es un entero. El constructor de Counter inicializa el estado en 0.

La clase Counter también contiene un método increment() que actualiza el estado del proveedor al incrementar el contador en 1.

Para crear un proveedor de estado utilizando StateNotifierProvider, debes pasar una instancia de la clase Counter al constructor de StateNotifierProvider:

final counterProvider = StateNotifierProvider((_) => Counter());

En este ejemplo, counterProvider es un proveedor de estado que utiliza la clase Counter para mantener el estado del contador. Para acceder al estado del contador en un widget, utilizas el widget Consumer o el método ref.watch:

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);
    return Column(
      children: [
        Text(counter.toString()),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('Increment'),
        ),
      ],
    );
  }
}

En este ejemplo, CounterWidget es un widget que utiliza ConsumerWidget para acceder al proveedor counterProvider y mostrar el estado del contador en un widget Text. El widget también contiene un botón que llama al método increment() de la clase Counter cuando se presiona.

Ámbito de los proveedores

En Riverpod, los proveedores pueden tener diferentes ámbitos, lo que significa que pueden estar disponibles en diferentes lugares de tu aplicación. Hay tres ámbitos principales de proveedores en Riverpod:

  • Global: los proveedores globales están disponibles en toda tu aplicación. Pueden ser accedidos por cualquier widget en cualquier lugar de tu aplicación.
  • Local: los proveedores locales están disponibles en un árbol de widgets específico. Solo pueden ser accedidos por widgets dentro de ese árbol de widgets.
  • Scoped: los proveedores de ámbito están disponibles en un árbol de widgets específico y se pueden utilizar para pasar datos a través de ese árbol de widgets.

Proveedores globales

Para crear un proveedor global en Riverpod, simplemente lo defines fuera de cualquier widget:

final greetingProvider = Provider((_) => 'Hello, world!');

En este ejemplo, greetingProvider es un proveedor global que está disponible en toda la aplicación.

Proveedores locales

Para crear un proveedor local en Riverpod, lo defines dentro de un widget que es el padre del árbol de widgets donde quieres utilizar el proveedor:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: ChildWidget(),
      overrides: [
        greetingProvider.overrideWithValue('Hello, Riverpod!'),
      ],
    );
  }
}

En este ejemplo, MyWidget es un widget que define un ámbito local para el proveedor greetingProvider. El widget MyWidget utiliza ProviderScope para crear un ámbito local y proporciona una lista de overrides que pueden anular el valor del proveedor global greetingProvider dentro de ese ámbito local.

En este caso, el valor del proveedor greetingProvider se anula con el valor "Hello, Riverpod!", que es el nuevo valor que se utilizará dentro de ChildWidget y todos sus descendientes.

Proveedores de ámbito

Los proveedores de ámbito son útiles cuando necesitas pasar datos a través de un árbol de widgets específico. Por ejemplo, puedes utilizar un proveedor de ámbito para pasar datos de autenticación a través de un árbol de widgets que contiene widgets que requieren autenticación.

Para crear un proveedor de ámbito en Riverpod, debes crear un nuevo widget que define el proveedor y luego colocar ese widget en el árbol de widgets donde quieres utilizar el proveedor:

class AuthProvider extends StateNotifier<String> {
  AuthProvider() : super('');

  void signIn(String token) => state = token;

  void signOut() => state = '';
}

final authProvider = StateNotifierProvider((_) => AuthProvider());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: MaterialApp(
        home: ProviderScope(
          child: AuthWidget(),
        ),
      ),
    );
  }
}

class AuthWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final auth = ref.watch(authProvider);
    if (auth.state == '') {
      return SignInWidget();
    } else {
      return SignedInWidget();
    }
  }
}

class SignInWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () => ref.read(authProvider.notifier).signIn('token'),
      child: Text('Sign In'),
    );
  }
}

class SignedInWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final auth = ref.watch(authProvider);
    return Column(
      children: [
        Text('Signed in with token: ${auth.state}'),
        ElevatedButton(
          onPressed: () => ref.read(authProvider.notifier).signOut(),
          child: Text('Sign Out'),
        ),
      ],
    );
  }
}

En este ejemplo, AuthProvider es un proveedor de ámbito que se utiliza para mantener el estado de autenticación de la aplicación. MyApp es el widget principal de la aplicación que utiliza ProviderScope para crear un ámbito global de proveedores.

Dentro de MyApp, ProviderScope se utiliza de nuevo para crear un ámbito de proveedores local que se utiliza para el widget AuthWidget. AuthWidget es un widget que utiliza ConsumerWidget para acceder al proveedor authProvider y determinar si el usuario ha iniciado sesión o no.

SignInWidget es un widget que muestra un botón para iniciar sesión y llama al método signIn() de AuthProvider cuando se presiona. SignedInWidget es un widget que muestra un mensaje que indica que el usuario ha iniciado sesión y muestra un botón para cerrar sesión.

En este ejemplo, el proveedor de ámbito authProvider se utiliza para pasar datos de autenticación a través de un árbol de widgets específico que contiene widgets que requieren autenticación. Al utilizar un proveedor de ámbito, puedes asegurarte de que los datos de autenticación sólo estén disponibles dentro del ámbito en el que se creó el proveedor. Esto ayuda a prevenir errores y hace que tu código sea más fácil de entender y mantener.

Proveedores de futuro (Future Provider)

Los proveedores de futuro son útiles cuando necesitas obtener datos asincrónicos en tu aplicación. En lugar de utilizar callbacks o streams para manejar los datos asincrónicos, puedes utilizar proveedores de futuro para encapsular los datos y acceder a ellos de manera más fácil y consistente.

Para crear un proveedor de futuro en Riverpod, debes utilizar la clase FutureProvider. FutureProvider toma una función que devuelve un futuro y utiliza ese futuro para proporcionar el valor del proveedor:

final greetingProvider = FutureProvider<String>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/greeting'));
  final body = json.decode(response.body);
  return body['greeting'];
});

class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final greeting = ref.watch(greetingProvider);
    return Text('Greeting: ${greeting.data}');
  }
}

En este ejemplo, greetingProvider es un proveedor de futuro que utiliza http.get() para obtener el valor del saludo desde una API. El valor del proveedor se actualiza automáticamente cuando el futuro se completa.

Dentro de MyWidget, se utiliza ConsumerWidget para acceder al valor del proveedor greetingProvider y mostrarlo en un widget de texto.

Conclusión

Riverpod es una biblioteca de gestión de estado para Flutter que ofrece una gran cantidad de características útiles, como la capacidad de crear proveedores globales y locales, la capacidad de utilizar datos asincrónicos y la capacidad de utilizar la programación reactiva para actualizar automáticamente tu interfaz de usuario.

En este artículo, hemos explorado algunos de los conceptos básicos de Riverpod y hemos visto algunos ejemplos simples de cómo utilizarlo en tus propias aplicaciones Flutter. Con su capacidad para simplificar el proceso de gestión de estado y hacer que tu código sea más fácil de entender y mantener, Riverpod es definitivamente una biblioteca que vale la pena aprender y utilizar en tus proyectos.

Ejemplos Prácticos

1. Autenticación de usuario

Supongamos que tienes una aplicación que requiere que los usuarios inicien sesión antes de poder acceder a ciertas funcionalidades. Puedes utilizar Riverpod para manejar el estado de autenticación de tu aplicación de la siguiente manera:

final authServiceProvider = Provider<AuthService>((ref) => AuthService());
final authStateProvider = StateProvider<AuthState>((ref) => AuthState.unauthenticated());

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authService = ref.watch(authServiceProvider);
    final authState = ref.watch(authStateProvider);

    return MaterialApp(
      home: authState.state.when(
        authenticated: (_) => HomePage(),
        unauthenticated: () => LoginPage(),
      ),
    );
  }
}

class LoginPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authService = ref.read(authServiceProvider);

    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            await authService.login();
            ref.read(authStateProvider).state = AuthState.authenticated();
          },
          child: Text('Login'),
        ),
      ),
    );
  }
}

class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authService = ref.read(authServiceProvider);

    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            await authService.logout();
            ref.read(authStateProvider).state = AuthState.unauthenticated();
          },
          child: Text('Logout'),
        ),
      ),
    );
  }
}

En este ejemplo, utilizamos un Provider llamado authServiceProvider para crear una instancia de AuthService, que es una clase que maneja la lógica de autenticación de nuestra aplicación.

También utilizamos un StateProvider llamado authStateProvider para manejar el estado de autenticación de nuestra aplicación. El estado de autenticación puede ser «autenticado» o «no autenticado», y lo utilizamos para determinar qué pantalla mostrar al usuario.

En MyApp, utilizamos ConsumerWidget para mostrar la pantalla correcta al usuario en función de su estado de autenticación.

En LoginPage, utilizamos ref.read() para obtener una instancia de AuthService y llamar a su método login(). Después de que el usuario se autentica correctamente, utilizamos ref.read() de nuevo para actualizar el estado de autenticación de nuestra aplicación.

En HomePage, hacemos lo mismo para manejar el proceso de cierre de sesión.

2. Tema de la aplicación

Supongamos que quieres permitir que el usuario seleccione un tema para tu aplicación. Puedes utilizar Riverpod para manejar el tema de tu aplicación de la siguiente manera:

final themeProvider = StateProvider<ThemeData>((ref) => ThemeData.light());

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final theme = ref.watch(themeProvider);

    return MaterialApp(
      theme: theme.state,
      home: HomePage(),
    );
  }
}

class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final theme = ref.watch(themeProvider);

    return Scaffold(
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () => ref.read(themeProvider).state = ThemeData.light(),          ),
          RaisedButton(
            child: Text('Light Theme'),
            onPressed: () => ref.read(themeProvider).state = ThemeData.light(),
          ),
          ElevatedButton(
            child: Text('Dark Theme'),
            onPressed: () => ref.read(themeProvider).state = ThemeData.dark(),
          ),
        ],
      ),
    );
  }
}

En este ejemplo, utilizamos un StateProvider llamado themeProvider para manejar el tema de nuestra aplicación. Por defecto, establecemos el tema en ThemeData.light(), pero el usuario puede cambiarlo a ThemeData.dark() si lo desea.

En MyApp, utilizamos ConsumerWidget para envolver nuestra aplicación en un MaterialApp y pasar el tema actual a través de la propiedad theme.

En HomePage, utilizamos ref.watch() para obtener el tema actual y mostrar dos botones que permiten al usuario cambiar el tema de la aplicación. Cuando el usuario presiona uno de los botones, utilizamos ref.read() para actualizar el estado de themeProvider y cambiar el tema de la aplicación en consecuencia.

3. Conexión a una API

Supongamos que tu aplicación necesita conectarse a una API para obtener datos. Puedes utilizar Riverpod para manejar la conexión a la API y el estado de carga de la siguiente manera:

final apiProvider = Provider<ApiClient>((ref) => ApiClient());
final apiStateProvider = StateProvider<ApiState>((ref) => ApiState.loading());

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final apiState = ref.watch(apiStateProvider);

    return MaterialApp(
      home: apiState.state.when(
        loading: () => LoadingScreen(),
        success: (data) => DataScreen(data),
        error: (errorMessage) => ErrorScreen(errorMessage),
      ),
    );
  }
}

class LoadingScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

class DataScreen extends StatelessWidget {
  final String data;

  DataScreen(this.data);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(data),
      ),
    );
  }
}

class ErrorScreen extends StatelessWidget {
  final String errorMessage;

  ErrorScreen(this.errorMessage);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(errorMessage),
      ),
    );
  }
}

class ApiState {
  final String? data;
  final String? errorMessage;

  ApiState.loading() : data = null, errorMessage = null;
  ApiState.success(this.data) : errorMessage = null;
  ApiState.error(this.errorMessage) : data = null;
}

class ApiClient {
  Future<String> fetchData() async {
    // Lógica para obtener datos de la API
  }
}

class ApiConsumer extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final apiState = ref.watch(apiStateProvider);
    final apiClient = ref.watch(apiProvider);

    if (apiState.state is ApiStateLoading) {
      apiClient.fetchData().then((data) {
        ref.read(apiStateProvider).state = ApiState.success(data);
      }).catchError((error) {
        ref.read(apiStateProvider).state = ApiState.error(error.toString());
      });
    }

    return Container();
  }
}

En este ejemplo, utilizamos un Provider llamado apiProvider para crear una instancia de `ApiClient y un StateProvider llamado apiStateProvider para manejar el estado de carga de la API. Por defecto, establecemos el estado en ApiState.loading().

En MyApp, utilizamos ConsumerWidget para envolver nuestra aplicación en un MaterialApp y mostrar diferentes pantallas según el estado actual de la API.

En LoadingScreen, mostramos un indicador de progreso mientras se cargan los datos.

En DataScreen, mostramos los datos obtenidos de la API.

En ErrorScreen, mostramos un mensaje de error si se produce un error al obtener los datos.

ApiState es una clase que utilizamos para manejar el estado de la API. Tiene tres posibles estados: loading, success y error.

ApiClient es una clase que utilizamos para conectarnos a la API y obtener los datos. En este ejemplo, utilizamos un método llamado fetchData() para obtener los datos de la API. Este método devuelve una Future<String> que contiene los datos obtenidos.

ApiConsumer es un ConsumerWidget que utilizamos para conectarnos a la API y actualizar el estado de apiStateProvider. En este ejemplo, utilizamos ref.watch() para obtener el estado actual de apiStateProvider y apiProvider. Si el estado actual es ApiState.loading(), llamamos a apiClient.fetchData() para obtener los datos de la API. Si se obtienen los datos correctamente, actualizamos el estado de apiStateProvider a ApiState.success(data). Si se produce un error, actualizamos el estado de apiStateProvider a ApiState.error(error.toString()).

En resumen, Riverpod es una biblioteca de gestión de estado para Flutter que permite manejar de manera eficiente el estado de la aplicación, independientemente del tamaño o complejidad de la misma. Con Riverpod, puedes crear proveedores para manejar el estado de la aplicación, conectar componentes a esos proveedores con ConsumerWidget y Consumer, y manejar cambios de estado de manera reactiva y eficiente. Además, Riverpod también proporciona una serie de características avanzadas, como proveedores diferidos, proveedores de futuro y proveedores de estado condicionales, para ayudar a simplificar la gestión del estado de la aplicación.

Copyright © 2023 Remi Rousselet.

Referencias