DEV Community

Alex
Alex

Posted on

Análisis comparativo de técnicas para la gestión de estado en Flutter: Provider, Riverpod, Bloc y GetX

Introducción

La gestión de estado en aplicaciones desarrolladas con Flutter resulta, en sus etapas iniciales, un proceso relativamente sencillo. No obstante, a medida que el proyecto evoluciona y la lógica de negocio se torna más compleja, surge la necesidad de compartir y sincronizar datos entre distintos context y Wingets. Este incremento en la complejidad exige la adopción de estrategias más estructuradas y eficientes para mantener el desarrollo escalable, mantenible y robusto.

Con el objetivo de responder a este desafío, la comunidad de Flutter ha desarrollado múltiples herramientas y metodologías que difieren en filosofía y curva de aprendizaje. Cada una de estas soluciones presenta ventajas y limitaciones particulares, lo que hace relevante un análisis comparativo que permita comprender sus diferencias y entender en qué casos es más conveniente elegir una.

En este artículo se presenta un estudio comparativo de algunos de los paquetes más utilizados en la gestión de estado en Flutter: Provider, Riverpod, BLoC y GetX, asi como el uso de setState() para este propósito y también InheritedNotifier. Considerarémos aspectos como su integración, mecanismo de reactividad, escalabilidad, flexibilidad y soporte de la comunidad.

Este artículo se basa en ejemplos prácticos que pueden consultarse en el repositorio flutter_state_managment en GitHub.


Motivación

La gestión de estado en Flutter está intrínsecamente condicionada por la naturaleza jerárquica del árbol de Widgets. En la práctica, es habitual que surja la necesidad de compartir información entre nodos que no mantienen una relación directa de padre e hijo, e incluso entre aquellos que no poseen vínculo jerárquico alguno. En tales circunstancias, las herramientas nativas de Flutter presentan limitaciones importantes.

En primer lugar, no existe un mecanismo nativo que permita la comunicación directa entre Widgets sin relación jerárquica. Esto obliga, en muchos casos, a transmitir datos a través de múltiples niveles intermedios del árbol, una práctica conocida como prop drilling, que incrementa la redundancia y la complejidad del código.

En segundo lugar, el objeto BuildContext está vinculado a una ubicación específica dentro de la jerarquía de Widgets. Como consecuencia, al realizar una navegación o reconstrucción significativa de la interfaz, el context asociado puede cambiar, dificultando el acceso a datos definidos en otra sección del árbol.

Para abordar estos escenarios utilizando únicamente las herramientas provistas por Flutter, existen dos enfoques principales: En primer lugar, tenemos el uso de setState() y el paso explícito de parámetros a lo largo del árbol. Segundo, la implementación de la familia de clases basadas en InheritedWidget, como InheritedNotifier que permite propagar cambios de estado a través del árbol.

Parámetros y setState()

Una de las estrategias más básicas para la gestión de estado en Flutter consiste en transmitir datos mediante parámetros entre Widgets, ya sea durante la navegación o a través de la construcción jerárquica de la interfaz. Las actualizaciones de dichos datos se realizan mediante callbacks que invocan el método setState(), de esta manera consiguiendo reactividad.

// https://github.com/aleax888/flutter_state_managment

// Dato que será compartido
int _counter = 0;

// Navegación que creará un nuevo contexto
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => FlutterSetStateTwinPage(
      counter: _counter, // Enviar dato
      title: "widget.title,"
      increment: (int c) { // Call Back
        setState(() {
          _counter = c;
        });
      },
      decrement: (int c) { // Call Back
        setState(() {
          _counter = c;
        });
      },
    ),
  ),
);

Enter fullscreen mode Exit fullscreen mode

Ventajas

  • No requiere el aprendizaje de librerías adicionales ni la adopción de patrones arquitectónicos complejos, lo que la convierte en una solución inmediata e ideal para proyectos pequeños o prototipos.

Limitaciones

  • Dificulta escalar el proyecto porque genera prop drilling.
  • Reduce la eficiencia de la app porque el uso desmedido de setState() realiza actualizaciones de la interfaz de manera innecesaria.
  • Dificulta realizar cambios porque acopla fuertemente los Widgets.

InheritedNotifier

El uso de InheritedNotifier consiste en envolver la aplicación con un Widget que implemente esta clase, de manera que el BuildContext incluya un objeto notificador capaz de exponer los valores necesarios para la interfaz.

// https://github.com/aleax888/flutter_state_managment

// Uso de InheritedNotifier para añadir al contexto un notificador
class CounterInheritedNotifier extends InheritedNotifier<CounterNotifier> {
  const CounterInheritedNotifier({
    super.key,
    required super.notifier,
    required super.child,
  });

  // El metodo [dependOnInheritedWidgetOfExactType] verifica en el árbol
  // si existe el notificador, en caso que si lo retorna,y si no retorna null 
  static CounterInheritedNotifier? maybeOf(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<CounterInheritedNotifier>();
  }

  // Metodo para obtener el notificador desde el contexto 
  // o un error en su defecto
  static CounterNotifier of(BuildContext context) {
    final CounterInheritedNotifier? result = maybeOf(context);
    assert(
      result != null && result.notifier != null,
      'No Counter found in context',
    );
    return result!.notifier!;
  }
}
Enter fullscreen mode Exit fullscreen mode

Para implementar un Notifier, es habitual ubicarlo como padre del Widget raíz, de esta manera inyectándolo a la aplicación. Los Widgets que requieren consumir esta información lo hacen mediante .of(context), el cual obtiene la instancia actual del notificador desde el context.

Ventajas

  • Ligero en términos de uso de memoria y procesamiento, al aprovechar de forma eficiente la estructura del árbol de Widgets.

Limitaciones

  • Su aplicabilidad práctica se reduce a escenarios relativamente simples.

Paquetes para manejar el estado

Además de las soluciones nativas que ofrece Flutter, la comunidad ha desarrollado una amplia gama de librerías para gestión del estado. Estas herramientas buscan reducir la complejidad asociada al paso manual de datos, mejorar la reactividad de la interfaz y favorecer la escalabilidad de las aplicaciones.

Cada una de estas alternativas se fundamenta en una filosofía particular y presenta un conjunto específico de ventajas y limitaciones que influyen en su adopción. Entre los factores que las diferencian se encuentran el mecanismo de inyección de dependencias, la forma de propagar cambios, el alcance del estado y las capacidades adicionales que ofrecen más allá de la mera sincronización de datos.

En el presente análisis se examinarán cuatro de los paquetes más reconocidos en el ecosistema Flutter - Provider, Riverpod, BLoC y GetX - todos ellos con un respaldo significativo por parte de la comunidad y con reconocimiento explícito por el equipo oficial de Flutter. La evaluación se realizará bajo un esquema uniforme que contempla:

  • Descripción y filosofía de la herramienta.
  • Mecanismo de inyección en la aplicación.
  • Estrategia para lograr la reactividad.
  • Alcance y propagación del estado entre contextos.
  • Procedimiento para la actualización de datos.
  • Características adicionales relevantes.
  • Conclusión individual (ventajas, limitaciones y comparaciones).

Provider

Descripción y filosofía: Ampliamente recomendada en la documentación oficial. Su funcionamiento se basa principalmente en el uso de ChangeNotifier, el mismo mecanismo que emplea InheritedNotifier, extendiendo la funcionalidad nativa de Flutter manteniendo sus ventajas.

// https://github.com/aleax888/flutter_state_managment

class CounterProvider with ChangeNotifier {
  late int _counter;
  int get counter => _counter;

  void counterStarted({int? counter}) {
    _counter = counter ?? 0;
    notifyListeners();
  }

  void increment() {
    _counter++;
    notifyListeners();
  }

  void decrement() {
    _counter--;
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

Inyección en la aplicación: La adición de un Provider al context se realiza mediante el Widget ChangeNotifierProvider, que envuelve una parte de la interfaz (o la aplicación completa) y expone un objeto notificador a los Widgets descendientes.

Ámbito del estado: Además de envolver la raíz de la aplicación, es posible usar ChangeNotifierProvider.value() para compartir una misma instancia de proveedor entre distintos contextos, evitando así la necesidad de propagarlo desde el nivel superior de la jerarquía.

Mecanismo de reactividad: Un Widget puede reaccionar a cambios en el estado utilizando context.watch<CounterProvider>().counter, lo que provoca su reconstrucción automática cuando el valor cambia.

Actualización de datos: La mutación del estado se realiza a través de métodos definidos en el ChangeNotifier, como en este ejemplo context.read<CounterProvider>().increment().

Características adicionales

  • Soporte para inyección de múltiples Providers mediante MultiProvider.

Conclusión: Destaca por su curva de aprendizaje baja, su escaso boilerplate y su relevancia dentro de la documentación de Flutter, lo que lo convierte en una buena opción para proyectos pequeños o medianos. Sin embargo, no promueve de forma explícita una separación de responsabilidades (entre lógica de negocio, lógica de aplicación, lógica de presentación, etc.), lo que convierte en una herramienta flexible, pero abierta a malas prácticas.

Riverpod

Descripción y filosofía: Es la evolución de Provider, con el objetivo de resolver algunas de sus limitaciones y ofrecer una arquitectura más robusta. Introduce distintos tipos de Providers para adaptarse a escenarios específicos, como estado inmutable, estado asíncrono o valores derivados.

// https://github.com/aleax888/flutter_state_managment

class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;

  void increment() => state++;

  void decrement() => state--;
}

final counterProvider = NotifierProvider<CounterNotifier, int>(() {
  return CounterNotifier();
});
Enter fullscreen mode Exit fullscreen mode

Inyección en la aplicación: Para habilitar Riverpod, la aplicación debe envolverse con el widget ProviderScope.

Mecanismo de reactividad: Para que un Widget tenga acceso a un Provider debe heredar la clase ConsumerWidget para obtener acceso a la referencia ref y poder suscribirse usando ref.watch(provider).

Actualización de datos: Dependiendo del tipo de proveedor utilizado, el estado se actualiza mediante métodos del Provider a través de la referencia ref.read(provider).increment().

Características adicionales

  • Soporte para hooks mediante el paquete hooks_riverpod, lo que facilita la migración a desarrolladores con experiencia en React.

Conclusión: Ofrece una mayor escalabilidad y un diseño más robusto que Provider, con una API que resulta familiar para desarrolladores provenientes de React. Sin embargo, presenta una curva de aprendizaje más pronunciada y, al igual que Provider, no impone una separación estricta de responsabilidades.

BLoC

Descripción y filosofía: Es un patrón arquitectónico ampliamente adoptado en Flutter para separar la lógica de negocio, la capa de presentación y la capa de datos. Se basa en el uso de streams y eventos para manejar el flujo de datos, promoviendo así un desarrollo reactivo y altamente escalable y robusto. Su implementación más popular es el paquete flutter_bloc.

// https://github.com/aleax888/flutter_state_managment

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterInitial()) {
    on<CounterStarted>(_onStarted);
    on<CounterIncrementPressed>(_onIncrementPressed);
    on<CounterDecrementPressed>(_onDecrementPressed);
  }

  late int _counter;
  int get counter => _counter;

  void _onStarted(CounterStarted event, Emitter<CounterState> emit) {
    _counter = event.counter ?? 0;
    emit(CounterLoadSuccess(counter: _counter));
  }

  void _onIncrementPressed(
    CounterIncrementPressed event,
    Emitter<CounterState> emit,
  ) {
    _counter++;
    emit(CounterLoadSuccess(counter: _counter));
  }

  void _onDecrementPressed(
    CounterDecrementPressed event,
    Emitter<CounterState> emit,
  ) {
    _counter--;
    emit(CounterLoadSuccess(counter: _counter));
  }
}
Enter fullscreen mode Exit fullscreen mode

Inyección en la aplicación: La inyección de un bloc se realiza mediante el Widget BlocProvider.

Ámbito del estado: Además de inyectar en la raíz de la aplicación, es posible compartir una instancia existente entre diferentes context utilizando BlocProvider.value.

Mecanismo de reactividad: Para reconstruir la interfaz en respuesta a cambios en el estado, se emplea BlocBuilder<CounterBloc, CounterState>, que escucha las emisiones del bloc y actualiza únicamente la parte de la interfaz afectada, siendo posible ignorar cambios de estado para optimizar la reconstrucción de la interfaz.

Actualización de datos: El cambio del estado en BLoC se realiza mediante eventos con BlocProvider.of<CounterBloc>(context).add(). El bloc procesa el evento, emite un nuevo estado y notifica a los Widgets suscritos.

Características adicionales

  • Reactividad fuera del árbol de Widgets con BlocListener manejar lógica en el cambio de estado fuera de la interfaz y combinación de BlocListener y BlocBuilder con BlocConsumer.
  • Posibilidad de inyectar múltiples blocs mediante MultiBlocProvider.
  • Compatibilidad con context.read() , etc.
  • Posibilidad de testing y debbugging con BlocObserver.

Conclusión: BLoC ofrece una arquitectura significativamente robusta, escalable y con clara separación de responsabilidades, siendo ideal para aplicaciones grandes y equipos de trabajo. No obstante, presenta una curva de aprendizaje elevada porque hace falta aprender tanto el patrón de diseño como el paquete, además de un importante nivel de boilerplate en comparación con soluciones como Provider o Riverpod.

Cubit

Descripción y filosofía: Es una variante simplificada del patrón BLoC, incluida en el paquete flutter_bloc. A diferencia de BLoC, que se basa en el envío y procesamiento de eventos, Cubit expone métodos directos que actualizan y emiten nuevos estados. Este enfoque reduce la cantidad de boilerplate.

// https://github.com/aleax888/flutter_state_managment

class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(CounterInitial());

  late int _counter;
  int get counter => _counter;

  void counterStarted({int? counter}) {
    _counter = counter ?? 0;
    emit(CounterLoadSuccess(counter: _counter));
  }

  void increment() {
    _counter++;
    emit(CounterLoadSuccess(counter: _counter));
  }

  void decrement() {
    _counter--;
    emit(CounterLoadSuccess(counter: _counter));
  }
}
Enter fullscreen mode Exit fullscreen mode

Todos los aspectos que estamos exponiendo, tanto la inyección, reactividad, actualización de datos y características adicionales, se maneja exactamente de la misma manera que con un bloc, bajo esa afirmación omitiremos esos apartados.

Conclusión: Es una solución más ligera y accesible que BLoC, reduciendo el boilerplate sin renunciar a la separación de responsabilidades. Es adecuado para casos donde se requiere simplicidad y rapidez de implementación, pero con un marco arquitectónico sólido. Sin embargo, en escenarios de gran complejidad, BLoC continúa siendo preferible debido a su mayor expresividad y control mediante eventos.

GetX Reactive State

Descripción y filosofía: Es un paquete de propósito general que ofrece, entre otras funcionalidades, un enfoque para la gestión de estado en Flutter. En su modalidad Reactive State, se basa en la reactividad automática, de tal manera que los Widgets observan a una variable y se reconstruyen en cada cambio.

// https://github.com/aleax888/flutter_state_managment

class CounterController extends GetxController {
  late RxInt _counter;
  int get counter => _counter.value;

  void counterStarted({int? counter}) {
    _counter = counter?.obs ?? 0.obs;
  }

  void increment() {
    _counter++;
  }

  void decrement() {
    _counter--;
  }
}
Enter fullscreen mode Exit fullscreen mode

Inyección en la aplicación: La creación y exposición de controladores se realiza con Get.put<CounterController>(CounterController()), lo que registra una instancia que podrá ser accedida desde cualquier parte de la aplicación. A diferencia de otros enfoques, no es necesario envolver la aplicación con un Widget que cumpla la función de inyección, ya que GetX administra internamente la inyección de dependencias sin usar el context.

Ámbito del estado: El acceso a instancias compartidas se logra mediante Get.find<CounterController>(), lo que permite obtener la instancia del controlador inyectado desde cualquier parte de la aplicación, incluso fuera del árbol de Widgets

Mecanismo de reactividad: Para lograr la reactividad, se utiliza el Widget GetX<CounterController>, el cual escucha los cambios en las variables reactivas definidas en el controlador y provoca la reconstrucción.

Actualización de datos: La actualización del estado se realiza directamente desde una instancia de controlador, modificando las variables observables a través de esta, ya sea invocando funciones o accediendo directamente al valor (no recomendable).

Características adicionales

  • GetX es un microframework, contando con funcionalidades como enrutamiento, manejo de dependencias, etc.
  • Otra forma de lograr reactividad usando variables observables (Rx) además del Widget GetX también se puede usar Obx, la diferencia es que Obx observa directamente una variable, a diferencia de GetX que depende de las actualizaciones de un controlador.
  • Si bien GetX no permite la inyección de un mismo controlador más de uno a la vez, esta limitación se puede superar usando el parámetro tag en las funciones .find().put() , consiguiendo inyectar la misma clase de un controlador las veces que sean necesarias.

Conclusión: La modalidad Reactive State de GetX proporciona un mecanismo extremadamente simple y directo para manejar la reactividad, con bajo boilerplate y sin necesidad de estructuras intermedias como eventos o Notifiers. No obstante, GetX permite caer en malas prácticas debido a su flexibilidad, además de consumir más recursos que otras soluciones por su independencia del context.

GetX Simple State

Descripción y filosofía: La modalidad Simple State de GetX ofrece una gestión de estado controlada y ya no automatica a diferencia de su hermana reactiva. En lugar de depender de variables observables, se apoya en la notificación manual de cambios mediante el método update() del controlador.

// https://github.com/aleax888/flutter_state_managment

class CounterController extends GetxController {
  late int _counter;
  int get counter => _counter;

  void counterStarted({int? counter}) {
    _counter = counter ?? 0;
    update();
  }

  void increment() {
    _counter++;
    update();
  }

  void decrement() {
    _counter--;
    update();
  }
}
Enter fullscreen mode Exit fullscreen mode

Inyección en la aplicación: El controlador se registra de la misma manera usando Get.find() o Get.put().

Mecanismo de reactividad: Para reconstruir los Widgets cuando el estado cambia, se utiliza GetBuilder<CounterController>, el cual escucha las actualizaciones invocadas manualmente.

Actualización de datos: Igualmente se realiza a través de un controlador.

Características adicionales

  • Menor consumo de memoria en comparación con el estado reactivo, ya que no requiere variables observables.

Conclusión: El enfoque Simple State de GetX es adecuado para escenarios donde se busca optimizar la reconstrucción de los Widgets, ya que se tiene control sobre la actualización del estado.

GetX Mixin State

Descripción y filosofía: La modalidad Mixin State de GetX está basada en la clase StateMixin<T>, que añade la capacidad de manejar estados según el patrón BLoC (no olvidemos la diferencia entre el patrón BLoC y el paquete flutter_bloc). Esto permite no solo escuchar cambios de valores, sino también gestionar diferentes escenarios de forma declarativa (cargando, vacío, error, datos) en la interfaz.

// https://github.com/aleax888/flutter_state_managment

class CounterController extends GetxController with StateMixin<int> {
  late int _counter;
  int get counter => _counter;

  void counterStarted({int? counter}) {
    _counter = counter ?? 0;
    change(_counter, status: RxStatus.success());
  }

  void increment() {
    _counter++;
    change(_counter, status: RxStatus.success());
  }

  void decrement() {
    _counter--;
    change(_counter, status: RxStatus.success());
  }
}
Enter fullscreen mode Exit fullscreen mode

Mecanismo de reactividad: La reactividad se logra mediante el uso de .obx() desde una instacia de un controlador, es un Widget builder especializado que reacciona automáticamente a los cambios en el estado administrado por el StateMixin.

Actualización de datos: Las actualizaciones a los datos se realizan igual a las otras dos modalidades, pero el controlador debe enviar los nuevos valores al estado usando change() junto con un tipo de estado como RxStatus.error().

Conclusión: El enfoque Mixin State es especialmente útil en aplicaciones que necesitan manejar múltiples estados de visualización (ej. loading → data → error) de manera declarativa y reactiva. Si bien añade una capa extra de complejidad respecto a los enfoques Simple State y Reactive State, mejora notablemente la claridad del código en escenarios de interacción con datos asincrónicos.


Tabla Comparativa


Conclusiones

Elegir correctamente la manera de hacer gestión del estado es Flutter es vital para llevar a cabo un proyecto, y para tomar la decisión podemos hacernos algunas preguntas como estas: ¿Qué maneras conocen los miembros del equipo? ¿Hay tiempo para aprender? ¿Cuán robusta necesita ser la aplicación? ¿Cuál es el alcance del proyecto? ¿Cuánto tiempo se dispone para el desarrollo?. Estas preguntas tienen sentido porque las diferentes maneras que hemos expuesto afectan directamente al desarrollo, dependiendo de qué tan tardado es aprenderlas, como por ejemplo BLoC que es la más tardada de aprender, o Provider que es la más limitada en funcionalidad.

En la computación con frecuencia se generan puntos de vista como "Esta herramienta es mejor que esta", pero la realidad es que cada herramienta tiene un objetivo y un propósito, como GetX que intenta cubrir prácticamente todas las necesidades del desarrollo de manera más sencilla posible, o BLoC que intenta alcanzar la mayor robustez. Entonces, difícilmente se puede decir "la herramienta A es mejor que B", lo que seguramente sucede es "para esta situación es A es más conveniente que B".

Los paquetes que se expusieron son un grupo reducido de las opciones más populares, pero existen muchas otras como MobX que tiene similitudes con Riverpod.

Este artículo se basa en ejemplos prácticos que pueden consultarse en el repositorio flutter_state_managment en GitHub.


Referencias

Top comments (0)