El estado en Flutter es cualquier dato que puede cambiar y que afecta lo que el usuario ve en pantalla.
Cuando el usuario toca un botón, escribe en un campo o carga información nueva, la interfaz debe actualizarse. Flutter usa un mecanismo llamado setState para hacer exactamente eso.
¿Qué es el estado?
El estado es la información dinámica de tu app. Por ejemplo, el número de productos en el carrito de Liverpool, el nombre que el usuario escribió en un formulario, o si un interruptor está activado o desactivado.
Hay dos tipos principales de estado:
| Tipo | Descripción | Ejemplo |
|---|---|---|
| Estado local | Solo le interesa a un widget específico | Un contador dentro de una pantalla |
| Estado global | Lo comparten varios widgets | El usuario que inició sesión en la app |
En esta lección te enfocas en el estado local, que es el más común cuando empiezas a desarrollar.
StatelessWidget vs StatefulWidget
Antes de usar setState, necesitas entender la diferencia entre los dos tipos de widget.
StatelessWidget es un widget que no cambia. Una vez que se dibuja en pantalla, su contenido es fijo. Un texto estático o un ícono son ejemplos típicos.
StatefulWidget es un widget que puede cambiar. Tiene un objeto State asociado. Cuando llamas a setState, Flutter vuelve a dibujar ese widget con los datos nuevos.
Estructura básica de un StatefulWidget
class MiWidget extends StatefulWidget {
@override
State<MiWidget> createState() => _MiWidgetState();
}
class _MiWidgetState extends State<MiWidget> {
// Variables de estado aquí
@override
Widget build(BuildContext context) {
// Interfaz aquí
return Container();
}
}
Nota que se crean dos clases: el widget público y su estado privado (por convención, con guion bajo al inicio).
¿Cómo funciona setState?
setState es un método que le indica a Flutter que las variables de estado cambiaron y que debe redibujar el widget.
Su sintaxis es:
setState(() {
// Cambia tus variables aquí
});
Todo lo que modifiques dentro del bloque de setState dispara la actualización visual. Si cambias una variable fuera de setState, la pantalla no se actualiza.
Ejemplo 1: Contador simple
Este ejemplo muestra un contador que sube cada vez que el usuario toca un botón. Es el ejemplo más básico del estado local.
class ContadorApp extends StatefulWidget {
@override
State<ContadorApp> createState() => _ContadorAppState();
}
class _ContadorAppState extends State<ContadorApp> {
int _contador = 0;
void _incrementar() {
setState(() {
_contador = _contador + 1;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Contador FEMSA")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Visitas registradas:"),
Text(
"$_contador",
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementar,
child: Icon(Icons.add),
),
);
}
}
Cada vez que el usuario toca el botón +, _incrementar llama a setState y Flutter redibuja el Text con el nuevo valor de _contador.
Ejemplo 2: Formulario de captura de datos
Este ejemplo es más cercano a una app real. Imagina que construyes un formulario sencillo para capturar el nombre y salario de un empleado de Bimbo.
class FormularioEmpleado extends StatefulWidget {
@override
State<FormularioEmpleado> createState() => _FormularioEmpleadoState();
}
class _FormularioEmpleadoState extends State<FormularioEmpleado> {
String _nombre = "";
double _salario = 0;
String _mensaje = "";
void _mostrarResumen() {
setState(() {
if (_nombre.isEmpty || _salario <= 0) {
_mensaje = "Completa todos los campos.";
} else {
_mensaje = "Empleado: $_nombre — Salario: \$${ _salario.toStringAsFixed(0).replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(m) => '\${m[1]},'
)}";
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Alta de empleado")),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
decoration: InputDecoration(labelText: "Nombre"),
onChanged: (valor) {
setState(() {
_nombre = valor;
});
},
),
SizedBox(height: 12),
TextField(
decoration: InputDecoration(labelText: "Salario mensual"),
keyboardType: TextInputType.number,
onChanged: (valor) {
setState(() {
_salario = double.tryParse(valor) ?? 0;
});
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _mostrarResumen,
child: Text("Ver resumen"),
),
SizedBox(height: 16),
Text(
_mensaje,
style: TextStyle(fontSize: 16, color: Colors.indigo),
),
],
),
),
);
}
}
Aquí hay tres variables de estado: _nombre, _salario y _mensaje. Cada TextField actualiza su variable correspondiente con setState en el callback onChanged. Al presionar el botón, _mostrarResumen valida y genera el texto final.
Un salario típico de $18,500 para un operador de planta se mostraría como:
Empleado: Juan Pérez — Salario: $18,500
Ciclo de vida del estado
Cuando usas StatefulWidget, Flutter sigue un ciclo de vida con métodos clave:
| Método | ¿Cuándo se ejecuta? | Uso común |
|---|---|---|
initState() |
Una sola vez al crear el widget | Inicializar variables, cargar datos |
build() |
Cada vez que cambia el estado | Dibujar la interfaz |
dispose() |
Cuando el widget se destruye | Liberar controladores y listeners |
Siempre que uses un TextEditingController, llama a dispose() para evitar fugas de memoria.
final TextEditingController _ctrl = TextEditingController();
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
Errores comunes
1. Cambiar variables fuera de setState
// ❌ Incorrecto: la pantalla no se actualiza
_contador = _contador + 1;
// ✅ Correcto
setState(() {
_contador = _contador + 1;
});
Si modificas una variable sin setState, el valor cambia en memoria pero Flutter no sabe que debe redibujar el widget.
2. Usar StatelessWidget cuando necesitas estado
Si ves que tu interfaz no responde a cambios, revisa que tu clase extienda StatefulWidget y no StatelessWidget. Es el error más frecuente en principiantes.
3. Poner lógica pesada dentro de setState
setState debe contener solo las asignaciones de variables. Nunca pongas llamadas a APIs, lecturas de archivos o cálculos complejos dentro del bloque. Haz esos procesos antes y guarda el resultado en una variable local.
// ❌ Incorrecto
setState(() {
_datos = await cargarDatosDelServidor(); // error: no puedes usar await aquí
});
// ✅ Correcto
final resultado = await cargarDatosDelServidor();
setState(() {
_datos = resultado;
});
4. No inicializar las variables de estado
Siempre da un valor inicial a tus variables. Si declaras String _nombre; sin valor, Dart lanzará un error en tiempo de compilación. Usa String _nombre = ""; o String? _nombre; si el valor puede ser nulo.
Resumen rápido
| Concepto | Descripción |
|---|---|
StatefulWidget |
Widget que puede cambiar su contenido |
State |
Clase que guarda las variables dinámicas |
setState() |
Método que actualiza la interfaz |
initState() |
Se ejecuta una vez al inicio |
dispose() |
Limpia recursos al cerrar el widget |
setState es la herramienta más directa para manejar el estado local. Para apps más grandes, existen soluciones como Provider o Riverpod, pero dominar setState es el primer paso obligatorio.