flutter-ciclo-vida-lifecycle

Ciclo de vida de State en Flutter: initState y dispose

  • 4 min

El ciclo de vida es la secuencia de estados por la que pasa un widget.

En el artículo anterior vimos que un StatefulWidget separa su configuración inmutable del objeto State, que se crea, se actualiza y se elimina junto con una posición del árbol.

A esta secuencia de eventos la llamamos el Ciclo de Vida (Lifecycle).

Entender cuándo ocurre cada fase es importante. Si accedes demasiado pronto a una dependencia del árbol, obtendrás un error. Si no liberas los recursos cuando el State desaparece, puedes provocar errores o fugas de memoria.

Vamos a diseccionar estas etapas.

Inicialización con initState()

Cuando Flutter inserta un StatefulWidget en el árbol de widgets, lo primero que hace (después del constructor) es ejecutar el método initState().

Este método se ejecuta una sola vez. Es el momento perfecto para la configuración inicial.

¿Para qué lo usamos?

  • Inicializar variables que dependen de la configuración del widget.
  • Suscribirse a Streams (flujos de datos).
  • Iniciar Controladores (como TextEditingController para formularios o AnimationController para animaciones).
  • Cargar datos iniciales (aunque ojo con hacer peticiones HTTP aquí sin gestionar la asincronía correctamente).
@override
void initState() {
  super.initState(); // ⚠️ SIEMPRE llama a super al principio
  
  // Aquí tu código de inicialización
  print("El Widget acaba de nacer");
  _iniciarTemporizador();
}
Copied!

El método build() aún no se ha ejecutado. No uses aquí dependOnInheritedWidgetOfExactType ni APIs que dependan de InheritedWidget, como Theme.of(context); para eso está didChangeDependencies(), que se llama justo después.

Reconstrucción con build() y setState()

Una vez inicializado, el widget entra en su fase activa. Flutter llama al método build() para pintar la interfaz.

Como vimos en el artículo anterior, cada vez que llamamos a setState(), el ciclo se repite en este punto: Flutter vuelve a ejecutar build() para reflejar los cambios.

No pongas lógica pesada ni inicies llamadas a APIs dentro de build(). Flutter puede invocarlo muchas veces, por lo que ese método debe ser rápido y no tener efectos secundarios.

Limpieza con dispose()

Aquí es donde muchos desarrolladores fallan. Cuando un widget deja de ser necesario (por ejemplo, porque el usuario navegó a otra pantalla o borró un elemento de una lista), Flutter lo destruye.

Justo antes de destruirlo, llama al método dispose().

Este es el momento de limpiar. Si abriste un grifo en el initState, tienes que cerrarlo en el dispose.

¿Qué hay que limpiar?

  • Timers: Si no los paras, seguirán contando en segundo plano aunque la pantalla ya no exista, lanzando errores.
  • Controllers: Los controladores de texto y animaciones ocupan memoria nativa. Hay que cerrarlos.
  • Streams: Debes cancelar las suscripciones para dejar de escuchar datos.
@override
void dispose() {
  // Limpieza antes de destruir
  _timer.cancel(); 
  _controller.dispose();
  
  super.dispose(); // ⚠️ SIEMPRE llama a super al final
  print("El Widget ha muerto y limpiamos la memoria");
}
Copied!

Ejemplo práctico: un cronómetro

Vamos a ver un ejemplo donde el ciclo de vida importa de verdad. Haremos un texto que cuenta segundos.

Si no usamos dispose, y el usuario sale de la pantalla, el cronómetro seguiría contando en la memoria del teléfono, gastando recursos innecesariamente (Memory Leak).

import 'dart:async'; // Necesario para Timer
import 'package:flutter/material.dart';

class CronometroWidget extends StatefulWidget {
  @override
  _CronometroWidgetState createState() => _CronometroWidgetState();
}

class _CronometroWidgetState extends State<CronometroWidget> {
  int _segundos = 0;
  Timer? _timer; // Variable para controlar el tiempo

  @override
  void initState() {
    super.initState();
    // NACIMIENTO: Iniciamos el timer
    _iniciarCuenta();
  }

  void _iniciarCuenta() {
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      // VIDA: Actualizamos el estado cada segundo
      setState(() {
        _segundos++;
      });
    });
  }

  @override
  void dispose() {
    // MUERTE: ¡Importantísimo! Cancelar el timer
    // Si no hacemos esto, el timer intentará hacer setState() 
    // en un widget que ya no existe y la app crasheará.
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Ciclo de Vida")),
      body: Center(
        child: Text(
          '$_segundos',
          style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }
}
Copied!