Ciclo de vida y Rendimiento de StatefulWidget
Última actualización:
Conceptos Clave:
-
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”.
- Imagina que un
-
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 (subuild
method). Si el pirata encuentra un tesoro, elState
dice “ahora dibuja al pirata con una sonrisa y una moneda de oro”.
- El
-
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 llamadocreateState()
. - 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.
- Cuando Flutter necesita que “Juan Pérez” (
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á acreateState()
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.
- Widgets que se configuran una vez (como el fondo de una escena) y no cambian mucho. No necesitan llamar a
-
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
delState
) 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.