6 formas de gestionar el estado en aplicaciones Flutter (Con ejemplos de código)
Cómo gestionar el estado en Flutter
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:
- 🧱
setState()
— El enfoque más sencillo incorporado - 🏗️ InheritedWidget — La base de Flutter para la propagación del estado
- 🪄 Provider — La solución recomendada para la mayoría de las apps
- 🔒 Riverpod — Evolución moderna y segura en tiempo de compilación de Provider
- 📦 Bloc — Para aplicaciones escalables y empresariales
- ⚡ GetX — Solución ligera todo en uno
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
- Documentación oficial de Flutter sobre gestión de estado
- Paquete Provider
- Documentación de Riverpod
- Biblioteca BLoC
- Documentación de GetX