¿Por qué pasar de Provider a Riverpod en Flutter?


Avatar de Pedro Cortez

Ahora que sabes gestionar tus variables con setState y Provider, ¿por qué no dar un paso más y hacer tu código más limpio con Riverpod? En esta guía, te explico las ventajas de este método y cómo usarlo para la gestión del estado de tu aplicación Flutter.


Riverpod en Flutter

¿Cuál es la diferencia entre Provider y Riverpod en Flutter?

En un artículo anterior, exploramos la gestión del estado con Provider, que simplifica el compartir datos en una aplicación Flutter en comparación con setState. Provider es excelente para proyectos de tamaño medio, ofreciendo un enfoque reactivo y una buena escalabilidad. Sin embargo, tiene algunas limitaciones:

  • Gestionar muchos datos conectados se convierte en un dolor de cabeza: Imagina que tu aplicación tiene que trabajar con varios Providers. Por ejemplo, una clase para contar los clics y otra para mostrar un mensaje según ese número. Esto sigue siendo factible si no hay demasiados Providers para gestionar al mismo tiempo, pero puede volverse más complejo a medida que el proyecto crece. Riverpod, actúa como el director de orquesta y organiza todo eso sin que tengas que hacer nada.
  • Provider está demasiado vinculado al «contexto» (BuildContext): Imagina que tu aplicación es una casa con muchas habitaciones (widgets). Con Provider (que actúa como la caja de herramientas), para acceder al estado de tus datos, siempre tienes que saber en qué habitación estás, eso es lo que se llama el BuildContext. Es como si tuvieras que gritar constantemente «¿Dónde estoy?» para tomar tus herramientas. Esta solución no es demasiado restrictiva en la mayoría de los casos, pero puede volverse engorrosa al probar código o mover cosas con facilidad. Riverpod, por su parte, da acceso directo a la caja de herramientas sin pedir el contexto.
  • Escribir código con Provider puede volverse largo y repetitivo: Con Provider, a menudo tendrás que usar métodos como Consumer o Provider.of para indicar a la aplicación que un dato ha cambiado y necesita ser actualizado. Esto no es tan problemático en un proyecto de tamaño medio, pero puede agregar muchas más líneas de código en proyectos más complejos.

Ventajas de Riverpod sobre Provider

Para entender mejor las ventajas de Riverpod sobre Provider, tomemos como ejemplo un botón que cuenta el número de veces que se hace clic sobre él. Con Provider, se vería así:

// Clase para gestionar el estado
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterManager extends ChangeNotifier {
  int count = 0;
  void increment() {
    count++;
    notifyListeners(); // Notificar a los widgets
  }
}

// main.dart
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterManager(),
      child: MaterialApp(home: CounterPage()),
    ),
  );
}

// counter_page.dart
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Consumer<CounterManager>(
              builder: (context, counter, _) => Text('Número: ${counter.count}'),
            ),
            ElevatedButton(
              onPressed: () => Provider.of<CounterManager>(context, listen: false).increment(),
              child: Text('¡Haz clic!'),
            ),
          ],
        ),
      ),
    );
  }
}

Y ahora, aquí está el mismo código, pero utilizando Riverpod para la gestión del estado:

// counter_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final counterProvider = StateProvider<int>((ref) => 0);

// main.dart
void main() {
  runApp(ProviderScope(child: MaterialApp(home: CounterPage())));
}

// counter_page.dart
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Número: $count'),
            ElevatedButton(
              onPressed: () => ref.read(counterProvider.notifier).state++,
              child: Text('¡Haz clic!'),
            ),
          ],
        ),
      ),
    );
  }
}

Como puedes ver, las ventajas de Riverpod sobre Provider son:

  • Menos código: No es necesario crear una clase completa como con Provider. Un simple StateProvider es suficiente.
  • Más directo: Con Riverpod, accedes y modificas el estado en una línea (ref.watch y ref.read), sin pasar por Consumer ni notifyListeners().
  • Menos complicaciones: Provider depende del BuildContext y puede ser pesado de manejar en una gran aplicación. Riverpod, por su parte, es independiente y va directo al grano.

¿Cómo configurar Riverpod para tu aplicación Flutter?

Para entender cómo configurar Riverpod, imaginemos una aplicación que tiene una página con un campo de texto donde el usuario escribe una palabra, y una lista que se actualiza en tiempo real con las palabras enviadas.

Paso 1: Añadir la dependencia Riverpod

Primero, asegúrate de añadir el paquete flutter_riverpod a tu proyecto. En tu archivo pubspec.yaml, incluye:

dependencies:
  flutter_riverpod: ^2.5.1

Luego, ejecuta flutter pub get para instalar la dependencia.

Paso 2: Crear los providers para gestionar el estado

Riverpod utiliza «providers» para almacenar y gestionar los datos de tu aplicación. Vamos a crear dos providers: uno para la palabra actual y otro para la lista de palabras.

import 'package:flutter_riverpod/flutter_riverpod.dart';

// Provider para la palabra actual
final currentWordProvider = StateProvider<String>((ref) => '');

// Provider para la lista de palabras
final wordListProvider = StateProvider<List<String>>((ref) => []);

Aquí, currentWordProvider contiene la palabra que estás escribiendo, y wordListProvider guarda la lista de palabras añadidas. Estos providers actúan como cajas de herramientas accesibles en toda la aplicación, y Riverpod se encarga de actualizarlos automáticamente.

Paso 3: Integrar Riverpod en el árbol de widgets

Para que estos providers sean utilizables, coloca un ProviderScope en la parte superior de tu árbol de widgets, generalmente en main.dart.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'word_page.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo Riverpod',
      home: WordPage(),
    );
  }
}

El ProviderScope es como una «zona» que permite que todos los widgets descendientes accedan a los providers.

Paso 4: Construir la interfaz con Riverpod

Crea una página (WordPage) con un campo de texto y una lista. Los widgets escucharán los cambios a través de Riverpod.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'word_provider.dart';

class WordPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final currentWord = ref.watch(currentWordProvider); // Escucha la palabra actual
    final wordList = ref.watch(wordListProvider);       // Escucha la lista

    return Scaffold(
      appBar: AppBar(title: Text('Gestión con Riverpod')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            // Campo de texto
            TextField(
              onChanged: (value) {
                ref.read(currentWordProvider.notifier).state = value;
              },
              decoration: InputDecoration(labelText: 'Ingresa una palabra'),
            ),
            SizedBox(height: 20),
            // Mostrar la palabra en tiempo real
            Text('Palabra actual: $currentWord'),
            SizedBox(height: 20),
            // Botón para añadir a la lista
            ElevatedButton(
              onPressed: () {
                if (currentWord.isNotEmpty) {
                  ref.read(wordListProvider.notifier).state = [...wordList, currentWord];
                  ref.read(currentWordProvider.notifier).state = '';
                }
              },
              child: Text('Añadir a la lista'),
            ),
            SizedBox(height: 20),
            // Lista de palabras
            Expanded(
              child: ListView.builder(
                itemCount: wordList.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(wordList[index]),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Explicaciones del código

  • ref.watch: Se utiliza para escuchar los providers (currentWordProvider y wordListProvider). Cuando sus valores cambian, los widgets afectados (el texto y la lista) se actualizan automáticamente.
  • ref.read: Permite modificar los providers sin escucharlos. Por ejemplo, actualizamos la palabra actual o agregamos una palabra a la lista. .notifier.state da acceso al valor para cambiarlo directamente.

¿Qué propiedades usar con Riverpod para modificar el estado?

Con Riverpod, puedes modificar el estado de tus providers de diferentes maneras, dependiendo del tipo de dato (número, lista, cadena, etc.) y lo que quieras hacer (añadir, eliminar, modificar).

Entre los métodos que más utilizarás están:

  • ref.read: Da acceso al provider para modificarlo.
  • .notifier: Es como el «control remoto» del provider.
  • .state: Permite notificar a Riverpod que un valor ha cambiado y avisa a los widgets que escuchan con ref.watch.

Aquí algunos ejemplos simples con StateProvider:

Modificar un valor simple (número, cadena, booleano)

final numberProvider = StateProvider<int>((ref) => 0);
// Incrementar un número
ref.read(numberProvider.notifier).state += 1;

//O para una cadena
final nameProvider = StateProvider<String>((ref) => 'Anna');
ref.read(nameProvider.notifier).state = 'Bob';

Agregar un elemento a una lista

Para agregar algo a una lista:

final numbersProvider = StateProvider<List<int>>((ref) => [1, 2, 3]);
// Agregar 4 a la lista
ref.read(numbersProvider.notifier).state = [...ref.read(numbersProvider.notifier).state, 4];
// Resultado: [1, 2, 3, 4]

… copia la lista actual para crear una nueva con el nuevo elemento.

Eliminar un elemento de una lista

Para eliminar un elemento:

final itemsProvider = StateProvider<List<String>>((ref) => ['manzana', 'pera']);
// Eliminar "manzana"
ref.read(itemsProvider.notifier).state = ref.read(itemsProvider.notifier).state.where((item) => item != 'manzana').toList();
// Resultado: ['pera']

.where filtra los elementos a conservar.

Multiplicar o transformar un valor

Para modificar un valor con un cálculo:

final scoreProvider = StateProvider<int>((ref) => 5);
// Multiplicar por 10
ref.read(scoreProvider.notifier).state *= 10;
// Resultado: 50

Vaciar o reiniciar

Para reiniciar:

final tasksProvider = StateProvider<List<String>>((ref) => ['tarea1', 'tarea2']);
// Vaciar la lista
ref.read(tasksProvider.notifier).state = [];
// Resultado: []
Avatar de Pedro Cortez