6 formas de gestionar el estado en aplicaciones Flutter (Con ejemplos de código)

Cómo gestionar el estado en Flutter

Índice

En este artículo, exploraremos seis formas populares de gestionar el estado en Flutter apps, incluyendo ejemplos reales y mejores prácticas:

Gestión del estado es uno de los temas más importantes - y debatidos - en el desarrollo de Flutter. Determina cómo tu app maneja los cambios en los datos y actualiza la interfaz de usuario de manera eficiente. Flutter te ofrece varias estrategias para gestionar el estado — desde las más simples hasta las altamente escalables.

Las estrategias para la gestión del estado en Flutter son:

  1. 🧱 setState() — El enfoque más sencillo incorporado
  2. 🏗️ InheritedWidget — La base de Flutter para la propagación del estado
  3. 🪄 Provider — La solución recomendada para la mayoría de las apps
  4. 🔒 Riverpod — Evolución moderna y segura en tiempo de compilación de Provider
  5. 📦 Bloc — Para aplicaciones escalables y empresariales
  6. GetX — Solución ligera todo en uno

flutter troubles mega tracktor


1. Usando setState() — Lo Básico

La forma más sencilla de gestionar el estado en Flutter es mediante el método incorporado setState() en un StatefulWidget.

Este enfoque es ideal para estado de UI local — donde el estado pertenece a un widget y no necesita compartirse en toda la aplicación.

Ejemplo: Aplicación de contador con setState()

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('setState Counter')),
      body: Center(
        child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Ventajas

  • Muy sencillo de implementar
  • Ideal para estado local o temporal de UI
  • Sin dependencias externas

Desventajas

  • No escala para aplicaciones grandes
  • Difícil compartir estado entre widgets
  • Lógica mezclada con UI

Úsalo cuando:

  • Prototipando o construyendo widgets pequeños
  • Manejando estado de UI aislado (por ejemplo, alternando un botón, mostrando un modal)

2. InheritedWidget — La base de Flutter

InheritedWidget es el mecanismo de nivel bajo que Flutter utiliza para propagar datos por el árbol de widgets. La mayoría de las soluciones de gestión de estado (incluyendo Provider) se construyen encima de él.

Entender InheritedWidget te ayuda a comprender cómo funciona la gestión de estado de Flutter en el interior.

Ejemplo: Gestor de tema con InheritedWidget

import 'package:flutter/material.dart';

// El InheritedWidget que contiene los datos del tema
class AppTheme extends InheritedWidget {
  final bool isDarkMode;
  final Function toggleTheme;

  const AppTheme({
    Key? key,
    required this.isDarkMode,
    required this.toggleTheme,
    required Widget child,
  }) : super(key: key, child: child);

  // Método de acceso para obtener el AppTheme más cercano en el árbol de widgets
  static AppTheme? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppTheme>();
  }

  @override
  bool updateShouldNotify(AppTheme oldWidget) {
    return isDarkMode != oldWidget.isDarkMode;
  }
}

// Envoltura con estado para gestionar cambios de estado
class ThemeManager extends StatefulWidget {
  final Widget child;

  const ThemeManager({Key? key, required this.child}) : super(key: key);

  @override
  _ThemeManagerState createState() => _ThemeManagerState();
}

class _ThemeManagerState extends State<ThemeManager> {
  bool _isDarkMode = false;

  void _toggleTheme() {
    setState(() {
      _isDarkMode = !_isDarkMode;
    });
  }

  @override
  Widget build(BuildContext context) {
    return AppTheme(
      isDarkMode: _isDarkMode,
      toggleTheme: _toggleTheme,
      child: widget.child,
    );
  }
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ThemeManager(
      child: Builder(
        builder: (context) {
          final theme = AppTheme.of(context);
          return MaterialApp(
            theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
            home: HomeScreen(),
          );
        },
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final theme = AppTheme.of(context);
    
    return Scaffold(
      appBar: AppBar(title: Text('InheritedWidget Theme')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Theme: ${theme!.isDarkMode ? "Dark" : "Light"}',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => theme.toggleTheme(),
              child: Text('Toggle Theme'),
            ),
          ],
        ),
      ),
    );
  }
}

Ventajas

  • Incorporado en Flutter — sin dependencias externas
  • Rebuilds de widgets eficientes
  • Base para entender otras soluciones
  • Control directo sobre la lógica de propagación

Desventajas

  • Código de plantilla verboso
  • Requiere un StatefulWidget envoltura
  • Fácil cometer errores
  • No es amigable para principiantes

Úsalo cuando:

  • Aprendiendo cómo funciona la gestión de estado de Flutter internamente
  • Construyendo soluciones personalizadas de gestión de estado
  • Necesitas un control muy específico sobre la propagación del estado

3. Provider — La solución recomendada por Flutter

Cuando el estado de tu app necesita compartirse entre múltiples widgets, Provider viene al rescate.

Provider se basa en Inversión de control — en lugar de que los widgets posean el estado, un proveedor lo expone para que otros lo consuman. El equipo de Flutter recomienda oficialmente que se use para aplicaciones de tamaño medio.

Configuración

Agrega esto a tu pubspec.yaml:

dependencies:
  provider: ^6.0.5

Ejemplo: Aplicación de contador con Provider

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: MyApp(),
    ),
  );
}

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterModel>();
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.read<CounterModel>().increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Ventajas

  • Actualizaciones reactivas y eficientes
  • Separación clara entre UI y lógica
  • Bien documentado y respaldado por la comunidad

Desventajas

  • Slightly más plantilla que setState()
  • Proveedores anidados pueden volverse complejos

Úsalo cuando:

  • Necesitas estado compartido entre múltiples widgets
  • Quieres un patrón reactivo sin complejidad
  • Tu app está creciendo más allá del tamaño de prototipo

4. Riverpod — La evolución moderna de Provider

Riverpod es una completa reescritura de Provider que elimina su dependencia de BuildContext y agrega seguridad en tiempo de compilación. Está diseñado para resolver las limitaciones de Provider mientras mantiene la misma filosofía.

Configuración

Agrega esto a tu pubspec.yaml:

dependencies:
  flutter_riverpod: ^2.4.0

Ejemplo: Perfil de usuario con Riverpod

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

// Modelo
class UserProfile {
  final String name;
  final int age;
  
  UserProfile({required this.name, required this.age});
  
  UserProfile copyWith({String? name, int? age}) {
    return UserProfile(
      name: name ?? this.name,
      age: age ?? this.age,
    );
  }
}

// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
  ProfileNotifier() : super(UserProfile(name: 'Guest', age: 0));

  void updateName(String name) {
    state = state.copyWith(name: name);
  }

  void updateAge(int age) {
    state = state.copyWith(age: age);
  }
}

// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
  return ProfileNotifier();
});

// Provider computado
final greetingProvider = Provider<String>((ref) {
  final profile = ref.watch(profileProvider);
  return 'Hello, ${profile.name}! You are ${profile.age} years old.';
});

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: ProfileScreen());
  }
}

class ProfileScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final profile = ref.watch(profileProvider);
    final greeting = ref.watch(greetingProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Profile')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(greeting, style: TextStyle(fontSize: 20)),
            SizedBox(height: 30),
            TextField(
              decoration: InputDecoration(labelText: 'Name'),
              onChanged: (value) {
                ref.read(profileProvider.notifier).updateName(value);
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Age: ${profile.age}', style: TextStyle(fontSize: 18)),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () {
                    ref.read(profileProvider.notifier).updateAge(profile.age + 1);
                  },
                  child: Text('+'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    if (profile.age > 0) {
                      ref.read(profileProvider.notifier).updateAge(profile.age - 1);
                    }
                  },
                  child: Text('-'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Ventajas

  • Sin dependencia de BuildContext — accede al estado en cualquier lugar
  • Seguridad en tiempo de compilación — detecta errores antes del tiempo de ejecución
  • Mejor testabilidad
  • Potente composición de estado
  • Sin fugas de memoria

Desventajas

  • API diferente de los patrones tradicionales de Flutter
  • Curva de aprendizaje más pronunciada que Provider
  • Comunidad más pequeña (pero creciendo rápidamente)

Úsalo cuando:

  • Iniciando un nuevo proyecto de Flutter
  • Quieres seguridad de tipo y garantías en tiempo de compilación
  • Dependencias complejas y composición de estado
  • Trabajando en aplicaciones de tamaño medio a grande

5. BLoC (Business Logic Component) — Para aplicaciones empresariales

El patrón BLoC es una arquitectura más avanzada que separa completamente la lógica empresarial de la UI usando streams.

Es ideal para aplicaciones empresariales o de gran tamaño, donde las transiciones de estado predecibles y testables son esenciales.

Configuración

Agrega esta dependencia a tu proyecto:

dependencies:
  flutter_bloc: ^9.0.0

Ejemplo: Aplicación de contador con BLoC

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// Eventos
abstract class CounterEvent {}
class Increment extends CounterEvent {}

// BLoC
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: MaterialApp(home: CounterScreen()),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('Count: $count', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        child: Icon(Icons.add),
      ),
    );
  }
}

Ventajas

  • Escalable y mantenible para aplicaciones complejas
  • Separación clara de capas
  • Fácil de probar en unidades

Desventajas

  • Más plantilla
  • Curva de aprendizaje más pronunciada

Úsalo cuando:

  • Construyendo proyectos empresariales o a largo plazo
  • Necesitas lógica predecible y testable
  • Varios desarrolladores trabajan en diferentes módulos de la app

6. GetX — La solución ligera todo en uno

GetX es una solución de gestión de estado minimalista pero poderosa que también incluye enrutamiento, inyección de dependencias y utilidades. Es conocido por tener la menor cantidad de plantilla de todas las soluciones.

Configuración

Agrega esto a tu pubspec.yaml:

dependencies:
  get: ^4.6.5

Ejemplo: Lista de compras con GetX

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// Controlador
class ShoppingController extends GetxController {
  // Lista observable
  var items = <String>[].obs;
  
  // Contador observable
  var totalItems = 0.obs;

  void addItem(String item) {
    if (item.isNotEmpty) {
      items.add(item);
      totalItems.value = items.length;
    }
  }

  void removeItem(int index) {
    items.removeAt(index);
    totalItems.value = items.length;
  }

  void clearAll() {
    items.clear();
    totalItems.value = 0;
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Shopping List',
      home: ShoppingScreen(),
    );
  }
}

class ShoppingScreen extends StatelessWidget {
  // Inicializa el controlador
  final ShoppingController controller = Get.put(ShoppingController());
  final TextEditingController textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Shopping List'),
        actions: [
          Obx(() => Padding(
                padding: EdgeInsets.all(16),
                child: Center(
                  child: Text(
                    'Items: ${controller.totalItems}',
                    style: TextStyle(fontSize: 18),
                  ),
                ),
              )),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: textController,
                    decoration: InputDecoration(
                      hintText: 'Enter item name',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    controller.addItem(textController.text);
                    textController.clear();
                  },
                  child: Icon(Icons.add),
                ),
              ],
            ),
          ),
          Expanded(
            child: Obx(() {
              if (controller.items.isEmpty) {
                return Center(
                  child: Text('No items in your list'),
                );
              }
              
              return ListView.builder(
                itemCount: controller.items.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    leading: CircleAvatar(child: Text('${index + 1}')),
                    title: Text(controller.items[index]),
                    trailing: IconButton(
                      icon: Icon(Icons.delete, color: Colors.red),
                      onPressed: () => controller.removeItem(index),
                    ),
                  );
                },
              );
            }),
          ),
          if (controller.items.isNotEmpty)
            Padding(
              padding: EdgeInsets.all(16),
              child: ElevatedButton(
                onPressed: () {
                  Get.defaultDialog(
                    title: 'Clear List',
                    middleText: 'Are you sure you want to clear all items?',
                    textConfirm: 'Yes',
                    textCancel: 'No',
                    onConfirm: () {
                      controller.clearAll();
                      Get.back();
                    },
                  );
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  minimumSize: Size(double.infinity, 50),
                ),
                child: Text('Clear All'),
              ),
            ),
        ],
      ),
    );
  }
}

Ventajas

  • Mínima plantilla — desarrollo más rápido
  • Solución todo en uno (estado, enrutamiento, DI, snacks, diálogos)
  • No se necesita BuildContext
  • Muy ligero y rápido
  • Fácil de aprender

Desventajas

  • Menos “Flutter-like” en comparación con Provider/BLoC
  • Puede llevar a acoplamiento estrecho si no se tiene cuidado
  • Ecosistema más pequeño que Provider
  • Algunos desarrolladores lo consideran “demasiado mágico”

Úsalo cuando:

  • Prototipado rápido o desarrollo de MVP
  • Aplicaciones pequeñas a medianas
  • Quieres mínima plantilla
  • El equipo prefiere simplicidad sobre arquitectura estricta

Elegir la solución adecuada de gestión de estado

Solución Complejidad Plantilla Curva de aprendizaje Mejor para
🧱 setState() Baja Mínima Fácil Estado local simple, prototipos
🏗️ InheritedWidget Media Alta Media Aprender internos de Flutter, soluciones personalizadas
🪄 Provider Baja-Media Baja Fácil La mayoría de las apps, estado compartido
🔒 Riverpod Media Baja-Media Media Apps modernas, seguridad de tipo
📦 BLoC Alta Alta Pronunciada Apps empresariales, lógica empresarial compleja
GetX Baja Mínima Fácil Desarrollo rápido, MVPs

Guía rápida de decisión:

Complejidad de la app Enfoque recomendado Caso de uso ejemplo
🪶 Simple setState() o GetX Alternar botones, animaciones, widgets pequeños
⚖️ Media Provider o Riverpod Temas compartidos, ajustes de usuario, almacenamiento de datos
🏗️ Compleja BLoC o Riverpod E-commerce, apps de chat, dashboards financieros

Reflexiones finales

No existe un enfoque todo en uno para la gestión de estado en Flutter.

Aquí hay un enfoque práctico:

  • Empieza pequeño con setState() para widgets simples y prototipos
  • Aprende los fundamentos con InheritedWidget para entender cómo funciona Flutter internamente
  • Avanza a Provider a medida que tu app crece y necesita estado compartido
  • Considera Riverpod para nuevos proyectos donde la seguridad de tipo y los patrones modernos importan
  • Adopta BLoC para aplicaciones empresariales con lógica empresarial compleja
  • Prueba GetX cuando el desarrollo rápido y la mínima plantilla sean prioridades

Conclusión clave:

✅ No sobre ingeniería — usa setState() hasta que necesites más
✅ Provider y Riverpod son excelentes opciones para la mayoría de las aplicaciones
✅ BLoC destaca en equipos grandes y aplicaciones complejas
✅ GetX es ideal para velocidad, pero ten en cuenta el acoplamiento
✅ Entender InheritedWidget te ayuda a dominar cualquier solución

La clave es equilibrar simplicidad, escalabilidad y mantenibilidad — y elegir la herramienta adecuada para tus necesidades específicas, experiencia del equipo y requisitos del proyecto.

Enlaces a bibliotecas de gestión de estado de Flutter

Otros enlaces útiles