Ciclo de Vida y TextEditingController
En el desarrollo de aplicaciones móviles profesionales, los componentes con estado no son eternos: nacen cuando el usuario entra en una pantalla, reaccionan dinámicamente ante los eventos mientras la pantalla está activa y mueren cuando el usuario navega hacia atrás.
Comprender el Ciclo de Vida de State es indispensable para inicializar recursos correctamente y, sobre todo, para evitar fugas de memoria (Memory Leaks) al gestionar entradas de texto con TextEditingController.
1. La Línea de Tiempo del Ciclo de Vida en State
A diferencia de un StatelessWidget (que solo posee el método build), la clase State<T> atraviesa varias etapas bien delimitadas durante su existencia en el Element Tree:
Las 5 Etapas Esenciales
createState(): Invocado automáticamente por el framework para vincular el widget inmutable con su nueva instancia de estado.initState()(Inicialización Única):- Es el primer método que se ejecuta tras la creación del objeto
State. - Se ejecuta exactamente una sola vez en toda la vida del widget.
- Es el lugar obligatorio para instanciar controladores (
TextEditingController), suscribirse a flujos de datos (Streams) o iniciar temporizadores. - Regla estricta: Siempre debe iniciar llamando a
super.initState().
- Es el primer método que se ejecuta tras la creación del objeto
didChangeDependencies(): Se ejecuta inmediatamente después deinitState()y cada vez que cambia un objeto heredado del contexto (como el tema de color o la orientación de la pantalla).build()(Fase Activa): Devuelve la estructura de widgets a renderizar. Se ejecuta al inicio y se repite cada vez que se llama asetState().dispose()(Destrucción y Limpieza Terminal):- Se ejecuta cuando el objeto
Statese retira permanentemente del árbol (por ejemplo, cuando el usuario cierra la pantalla o navega hacia atrás conNavigator.pop). - Es el lugar donde debes liberar y destruir obligatoriamente todos los recursos pesados que abriste en
initState(). - Regla estricta: Siempre debe finalizar llamando a
super.dispose().
- Se ejecuta cuando el objeto
2. ¿Qué es un TextEditingController?
El widget TextField permite a los usuarios ingresar texto desde el teclado del dispositivo. Para poder leer lo que el usuario escribió, modificar el texto mediante programación o vaciar el campo tras enviar un formulario, Flutter utiliza un TextEditingController.
Un controlador actúa como un puente bidireccional:
- Lectura: Permite consultar el texto actual mediante la propiedad
controller.text. - Escritura: Permite cambiar el texto mediante
controller.text = 'Nuevo valor'ocontroller.clear(). - Escucha reactiva: Puede emitir eventos cada vez que el usuario presiona una tecla mediante
controller.addListener(...).
3. El Peligro Crítico de Fugas de Memoria (Memory Leaks)
Un TextEditingController se comunica con los servicios nativos de entrada de texto del sistema operativo (Android o iOS) y retiene punteros a listeners en la memoria RAM.
Si creas un TextEditingController en tu pantalla y no lo destruyes llamando a .dispose() dentro del método dispose() de tu clase State, el recolector de basura (Garbage Collector) de Dart no podrá liberar esa memoria.
Si el usuario abre y cierra la pantalla 20 veces al día, habrá 20 controladores huérfanos acumulándose en la memoria RAM del teléfono, provocando ralentizaciones, consumo excesivo de batería y, finalmente, el cierre forzado de la aplicación (App Crash por falta de memoria).
4. Implementación Completa: Formulario con Validación y Limpieza
A continuación se presenta un ejemplo completo que muestra la inicialización en initState(), el uso dentro del formulario y la liberación obligatoria en dispose():
- Formulario Reactivo
- Puntos Clave del Código
import 'package:flutter/material.dart';
class RegistroPage extends StatefulWidget {
const RegistroPage({super.key});
State<RegistroPage> createState() => _RegistroPageState();
}
class _RegistroPageState extends State<RegistroPage> {
// 1. Declaración de controladores
late final TextEditingController _nombreController;
late final TextEditingController _emailController;
// Estado local para mensajes de confirmación
String _mensajeResultado = '';
void initState() {
super.initState();
// 2. Inicialización obligatoria en initState (solo ocurre 1 vez)
_nombreController = TextEditingController();
_emailController = TextEditingController();
}
void dispose() {
// 3. LIMPIEZA OBLIGATORIA: Previene fugas de memoria
_nombreController.dispose();
_emailController.dispose();
super.dispose();
}
void _procesarFormulario() {
final nombre = _nombreController.text.trim();
final email = _emailController.text.trim();
// Validación sencilla de campos obligatorios
if (nombre.isEmpty || email.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Por favor completa todos los campos del formulario'),
backgroundColor: Colors.redAccent,
),
);
return;
}
setState(() {
_mensajeResultado = 'Usuario registrado: $nombre ($email)';
});
// 4. Limpieza de los campos tras el envío exitoso
_nombreController.clear();
_emailController.clear();
}
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Formulario de Registro',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Campo Nombre
TextField(
controller: _nombreController,
decoration: const InputDecoration(
labelText: 'Nombre Completo',
hintText: 'Ej. Juanita Pérez',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// Campo Correo
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Correo Electrónico',
hintText: 'usuario@icesi.edu.co',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Botón de Enviar
FilledButton.icon(
onPressed: _procesarFormulario,
icon: const Icon(Icons.check),
label: const Text('Registrar Usuario'),
),
if (_mensajeResultado.isNotEmpty) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF0FDF4),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF86EFAC)),
),
child: Text(
_mensajeResultado,
style: const TextStyle(color: Color(0xFF15803D), fontWeight: FontWeight.w600),
),
),
],
],
),
);
}
}
late final: Se utiliza para indicar al analizador de Dart que la variable no puede ser nula, que se inicializará eninitState()y que su referencia no cambiará jamás.controller.text.trim(): La función.trim()remueve espacios en blanco innecesarios al inicio o al final introducidos accidentalmente por el usuario.controller.clear(): Es un atajo equivalente a escribircontroller.text = '', dejando el campo visual vacío y listo para una nueva entrada.OutlineInputBorder(): Componente estándar de Google Material Design 3 que dibuja el recuadro con esquinas redondeadas alrededor del campo de entrada.