Saltearse al contenido

Conceptos Clave:

  1. StatefulWidget (El Actor):

    • Imagina que un StatefulWidget es como un actor (por ejemplo, “Juan Pérez”).
    • Juan Pérez siempre es Juan Pérez, no cambia su identidad (es inmutable). Él es la “descripción” de lo que se va a mostrar.
    • Por sí solo, Juan Pérez no “hace” nada en escena, solo “es”.
  2. State (El Papel y su Actuación):

    • El State es el papel que Juan Pérez interpreta en una obra (por ejemplo, “Pirata Valiente”).
    • Este papel SÍ puede cambiar: el pirata puede estar contento, luego enojado, puede encontrar un tesoro (su estado cambia). El State guarda toda la información que puede cambiar con el tiempo.
    • El State también es responsable de cómo se ve el actor en escena (su build method). Si el pirata encuentra un tesoro, el State dice “ahora dibuja al pirata con una sonrisa y una moneda de oro”.
  3. createState() (El Casting):

    • Cuando Flutter necesita que “Juan Pérez” (StatefulWidget) aparezca en la pantalla interpretando al “Pirata Valiente” (State), llama a un método especial llamado createState().
    • Este método es como un director de casting que dice: “Ok, para este actor Juan Pérez, necesitamos crearle el papel de Pirata Valiente”. Y así se crea un objeto State nuevo y fresco para ese papel.

Ilustración Simple:

+---------------------+
| StatefulWidget |
| (Actor: Juan Pérez) |
| (Es INMUTABLE) |
+--------|------------+
|
| llama a createState()
| (Flutter dice: "¡Acción!")
V
+---------------------+
| State Object |
| (Papel: Pirata) |
| (Guarda los cambios |
| y cómo se dibuja) |
| (Es MUTABLE) |
+---------------------+

¿Qué Pasa si el Actor (StatefulWidget) Aparece Varias Veces?

  • Si pones a “Juan Pérez” (StatefulWidget) en dos lugares diferentes de tu app (por ejemplo, en la parte de arriba y en la de abajo de la pantalla), Flutter llamará a createState() dos veces.
  • Esto significa que tendrás dos objetos State separados. Es como si Juan Pérez estuviera interpretando al “Pirata Valiente” en dos escenarios diferentes al mismo tiempo. Cada pirata tendría su propio estado (uno podría estar triste y el otro feliz).
    • Actor Juan Pérez (Widget) —> Papel Pirata 1 (State 1)
    • Actor Juan Pérez (Widget) —> Papel Pirata 2 (State 2)

¿Qué Pasa si Quitas al Actor y lo Vuelves a Poner?

  • Si quitas a “Juan Pérez” de la pantalla y luego lo vuelves a poner, Flutter generalmente llamará a createState() de nuevo, dándole un papel (State) completamente nuevo y fresco. Es como si el pirata anterior se hubiera ido y ahora llega uno nuevo para empezar desde cero.

Excepción: GlobalKey (El Contrato de Estrella)

  • Imagina que “Juan Pérez” tiene un GlobalKey. Es como un contrato de estrella que dice: “Este Juan Pérez interpretando a este Pirata Valiente es ÚNICO en toda la producción”.
  • Si mueves a este Juan Pérez (con su GlobalKey) de un lugar de la pantalla a otro muy rápidamente (en el mismo “instante” o “fotograma de animación”):
    • Flutter no crea un nuevo papel (State).
    • En lugar de eso, mueve al actor CON su papel actual y todo su estado (vestuario, humor, tesoros encontrados) al nuevo lugar. El pirata sigue siendo el mismo, con las mismas cosas, solo que en otra parte del escenario.
  • Esto es útil para no perder el estado cuando reorganizas cosas complejas en la pantalla.

Rendimiento (¿Por qué tanta complicación?)

  • Categoría 1 (Actores de Fondo / Escenografía):

    • Widgets que se configuran una vez (como el fondo de una escena) y no cambian mucho. No necesitan llamar a setState() (que es como decir “¡hey, director, he cambiado, vuelve a dibujarme!”).
    • Son baratos de mantener porque se “construyen” una vez y ya.
  • Categoría 2 (Actores Principales Interactivos):

    • Widgets que cambian a menudo porque el usuario interactúa con ellos (un botón que cambia de color) o porque dependen de otros datos que cambian.
    • Estos usan setState() para avisar que necesitan redibujarse.
    • Es importante que su “construcción” (el método build del State) sea rápida, porque se llamará muchas veces.

¿Por qué StatefulWidget es Inmutable (el Actor no cambia)?

  • Es más rápido para Flutter comparar si dos actores son “el mismo Juan Pérez” (chequeando si son la misma instancia) que revisar si todos los detalles de sus papeles (estados) son idénticos.
  • Si necesitas que el actor haga algo diferente, no cambias al actor directamente. Creas una nueva descripción del actor con las nuevas instrucciones, y Flutter decidirá si necesita un nuevo papel (State) o puede reusar el antiguo (raro, pero posible con GlobalKeys).

const (Actores Genéricos Pre-fabricados):

  • Si usas const para crear un widget (por ejemplo, un texto simple que nunca cambia: const Text("Hola")), Flutter es súper eficiente.
  • Si necesitas 10 veces el mismo const Text("Hola"), Flutter usa la misma instancia “pre-fabricada” para todos. Es como tener un montón de extras idénticos listos para usar, sin tener que crear uno nuevo cada vez. ¡Ahorra mucho trabajo!
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// Este widget raíz es StatelessWidget, es como el "Teatro" general.
// Es 'const' porque no tiene estado propio que cambie.
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo StatefulWidget',
theme: ThemeData(
primarySwatch: Colors.blue,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
foregroundColor: Colors.black,
)
)
),
home: DemoPage(), // La página principal de nuestra demostración
);
}
}
// ---------- NUESTRO ACTOR (StatefulWidget) y su PAPEL (State) ----------
class MyCounterWidget extends StatefulWidget {
final String id; // Para identificar la instancia en los logs
// El "Actor" (StatefulWidget) en sí es inmutable.
// Sus propiedades (como 'id') son 'final'.
// El constructor puede ser 'const' si todas sus propiedades son 'final'
// y se inicializan con valores constantes o constructores 'const'.
const MyCounterWidget({Key? key, required this.id}) : super(key: key);
// El "Casting" (createState): Cuando Flutter necesita mostrar este actor,
// llama a este método para crear su "Papel" (el objeto State).
@override
_MyCounterWidgetState createState() {
print("=> ($id) LLAMANDO A createState() - Creando un nuevo papel para el actor.");
return _MyCounterWidgetState();
}
}
class _MyCounterWidgetState extends State<MyCounterWidget> {
// Este es el "Papel" (State). Aquí es donde vive el estado mutable.
int _counter = 0; // El estado que puede cambiar (el pirata encuentra monedas)
@override
void initState() {
super.initState();
// Se llama UNA VEZ cuando el "Papel" (State) se crea y se inserta en el árbol.
// Como cuando el actor entra en escena por primera vez.
print("==> (${widget.id}) initState() - Papel inicializado. Contador: $_counter");
}
void _incrementCounter() {
// setState notifica a Flutter que el "Papel" ha cambiado
// y que la parte de la interfaz que depende de este estado necesita redibujarse.
// Es como si el actor dijera: "¡He encontrado una moneda! ¡Que todos lo vean!".
setState(() {
_counter++;
print("===> (${widget.id}) setState() - Contador ahora es: $_counter");
});
}
@override
Widget build(BuildContext context) {
// Se llama cada vez que el "Papel" necesita dibujarse.
// (Al inicio, y después de cada setState()).
// Describe cómo se ve el actor en escena con su estado actual.
print("====> (${widget.id}) build() - Dibujando con contador: $_counter");
return Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Contador "${widget.id}":',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 10),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Incrementar ${widget.id}'),
),
],
),
);
}
@override
void dispose() {
// Se llama cuando el "Papel" (State) se elimina permanentemente del árbol.
// Como cuando el actor sale de la obra definitivamente.
// Aquí se liberan recursos.
print("=====> (${widget.id}) dispose() - Papel eliminado.");
super.dispose();
}
}
// ---------- PÁGINA DE DEMOSTRACIÓN PARA VER TODO EN ACCIÓN ----------
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
bool _showSimpleCounter = true;
bool _showGlobalKeyCounter = true;
bool _globalKeyCounterIsAtTop = true;
// Esta es la "llave mágica" o "contrato de estrella".
// Debe ser 'final' y generalmente se declara en la clase State de quien la usa.
final GlobalKey<_MyCounterWidgetState> _globalCounterKey = GlobalKey<_MyCounterWidgetState>();
@override
Widget build(BuildContext context) {
print("\n--- RECONSTRUYENDO DemoPage ---");
return Scaffold(
appBar: AppBar(
title: Text('Demo Ciclo de Vida StatefulWidget'),
),
body: Center(
child: SingleChildScrollView( // Para evitar overflow si hay muchos elementos
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"1. Contador Simple (Sin GlobalKey)",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.deepPurple),
),
ElevatedButton(
onPressed: () {
setState(() {
_showSimpleCounter = !_showSimpleCounter;
});
},
child: Text(_showSimpleCounter ? 'Ocultar Contador Simple' : 'Mostrar Contador Simple'),
),
if (_showSimpleCounter)
MyCounterWidget(id: "Simple"), // Actor "Simple"
Divider(height: 30, thickness: 2),
Text(
"2. Contador con GlobalKey",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.teal),
),
ElevatedButton(
onPressed: () {
setState(() {
_showGlobalKeyCounter = !_showGlobalKeyCounter;
// Si lo mostramos, lo ponemos arriba por defecto para la demo
if (_showGlobalKeyCounter) _globalKeyCounterIsAtTop = true;
});
},
child: Text(_showGlobalKeyCounter ? 'Ocultar Contador GlobalKey' : 'Mostrar Contador GlobalKey'),
),
if (_showGlobalKeyCounter) // Solo mostrar el botón de mover si el contador es visible
ElevatedButton(
onPressed: () {
setState(() {
_globalKeyCounterIsAtTop = !_globalKeyCounterIsAtTop;
});
},
child: Text('Mover Contador GlobalKey'),
),
// Lugar 1 para el contador con GlobalKey
if (_showGlobalKeyCounter && _globalKeyCounterIsAtTop)
Container(
color: Colors.blue.withOpacity(0.1),
padding: EdgeInsets.all(8),
child: MyCounterWidget(
key: _globalCounterKey, // ¡Aquí está el contrato de estrella!
id: "ConGlobalKey", // Actor "ConGlobalKey"
),
),
if (_showGlobalKeyCounter) // Para que se vea que hay algo entre los dos lugares
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Text(
_globalKeyCounterIsAtTop ? "--- Espacio abajo ---" : "--- Espacio arriba ---",
style: TextStyle(fontStyle: FontStyle.italic),
),
),
// Lugar 2 para el contador con GlobalKey
if (_showGlobalKeyCounter && !_globalKeyCounterIsAtTop)
Container(
color: Colors.green.withOpacity(0.1),
padding: EdgeInsets.all(8),
child: MyCounterWidget(
key: _globalCounterKey, // ¡Mismo contrato de estrella!
id: "ConGlobalKey", // Mismo actor "ConGlobalKey"
),
),
Divider(height: 30, thickness: 2),
Text(
"Observaciones:",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Mira la consola (Debug Console en VS Code, Run en Android Studio) "
"para ver los logs de createState, initState, build y dispose.",
textAlign: TextAlign.center,
),
),
],
),
),
),
);
}
}

En resumen:

  • StatefulWidget (Actor): La descripción de “quién” va a estar. No cambia.
  • State (Papel): “Cómo” está el actor y “qué” está haciendo. Sí cambia y se encarga de cómo se ve.
  • Se separan para que Flutter pueda ser eficiente al actualizar la pantalla. Si solo cambia el “papel” (Estado), no necesita reevaluar toda la identidad del “actor” (Widget).
  • GlobalKey es un truco para mover un actor con su papel actual a otro sitio sin que pierda su estado.