Volver al inicio

Optimización de Flutter: evita reconstrucciones

El artículo analiza 8 errores comunes de optimización de apps Flutter: reconstrucciones extras, falta de const, lógica en build(), listas incorrectas. Se proporcionan ejemplos de código de correcciones y prácticas clave para 60 FPS estables.

Flutter sin congelamientos: 8 optimizaciones clave
Advertisement 728x90

Técnicas Prácticas para Acelerar Aplicaciones Flutter

Las aplicaciones Flutter a menudo sufren de tirones debido a reconstrucciones innecesarias, operaciones en el hilo de la interfaz y manejo ineficiente de listas. Este artículo desglosa errores clave y cómo solucionarlos: desde localizar actualizaciones hasta el uso adecuado de constructores y animaciones. Estas técnicas ayudan a mantener 60 FPS incluso en dispositivos de gama baja.

Minimizar Reconstrucciones Innecesarias de la Interfaz

Las reconstrucciones innecesarias son una causa principal de caídas de rendimiento. setState() reconstruye todo el subárbol de un StatefulWidget, incluyendo listas pesadas e imágenes. Esto conduce a recálculos de diseño y caídas de FPS.

Código Problemático:

Google AdInline article slot
class MyScreen extends StatefulWidget {
  @override
  State<MyScreen> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
  int counter = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Contador: $counter'),
        ElevatedButton(
          onPressed: () {
            setState(() {
              counter++; // reconstruye TODO el árbol
            });
          },
          child: Text('Incrementar'),
        ),
      ],
    );
  }
}

Solución: ValueListenableBuilder actualiza solo la parte dependiente de la interfaz.

class MyScreen extends StatelessWidget {
  final ValueNotifier<int> counter = ValueNotifier(0);
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ValueListenableBuilder<int>(
          valueListenable: counter,
          builder: (_, value, __) {
            return Text('Contador: $value');
          },
        ),
        ElevatedButton(
          onPressed: () => counter.value++,
          child: const Text('Incrementar'),
        ),
      ],
    );
  }
}

De manera similar, StreamBuilder, Selector de provider o BlocBuilder funcionan. El área de repintado se minimiza, reduciendo la carga de la CPU.

Usar Constructores const

Sin const, cada reconstrucción crea nuevos objetos de widget, forzando el GC y la CPU. En listas con cientos de iconos, textos y rellenos, esto es crítico.

Google AdInline article slot

Enfoque óptimo:

Widget build(BuildContext context) {
  return const Column(
    children: [
      Text('Hola'),
      Icon(Icons.home),
      Padding(
        padding: EdgeInsets.all(8),
        child: Text('Mundo'),
      ),
    ],
  );
}

Flutter reutiliza objetos const, reduciendo asignaciones. La ganancia es notable en interfaces complejas con miles de widgets pequeños.

Mover Lógica Fuera de build()

Ordenar, filtrar o analizar en build() se ejecuta en cada llamada—hasta 60 veces por segundo. Para 60 FPS, solo hay 16 ms por fotograma; operaciones pesadas causan tirones.

Google AdInline article slot

Enfoque correcto:

late List<int> filtered;
@override
void initState() {
  super.initState();
  final sorted = [...items]..sort((a, b) => a.compareTo(b));
  filtered = sorted.where((e) => e > 10).toList();
}
@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: filtered.length,
    itemBuilder: (_, i) => Text('${filtered[i]}'),
  );
}

La lógica se ejecuta una vez en initState o la capa de negocio. build() permanece declarativo.

Listas Perezosas con ListView.builder

ListView con children: [...] crea todos los elementos a la vez, incluso los invisibles. Con 100+ elementos, la memoria y el renderizado inicial sufren.

Recomendación:

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      final item = items[index];
      return ListTile(
        title: Text(item.title),
        subtitle: Text(item.subtitle),
      );
    },
  );
}

Los elementos se generan al desplazarse, la memoria se optimiza y el reciclaje funciona eficientemente.

Claves para Estabilidad de Listas

Sin una clave, Flutter no puede emparejar elementos durante actualizaciones de lista, causando reconstrucciones innecesarias y artefactos visuales.

Crítico para:

  • Reordenar elementos
  • Eliminación/inserción
  • Animaciones de transición
ListView.builder(
  itemCount: items.length,
  itemBuilder: (_, index) {
    final item = items[index];
    return ListTile(
      key: ValueKey(item.id),
      title: Text(item.title),
    );
  },
);

ValueKey(item.id) asegura actualizaciones correctas.

Animaciones Sin setState

Timer.periodic con setState cada 16–100 ms sobrecarga el hilo de la interfaz.

Herramienta estándar:

late AnimationController controller;
@override
void initState() {
  super.initState();
  controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 2),
  )..repeat();
}
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: controller,
    builder: (_, __) {
      return LinearProgressIndicator(value: controller.value);
    },
  );
}

AnimationController se sincroniza con los FPS del dispositivo, actualizando la interfaz mínima.

Dividir en Widgets Pequeños

Un StatefulWidget enorme se reconstruye completamente en cualquier cambio.

Ventajas de la composición:

  • Reconstrucciones localizadas
  • Reutilización de código
  • Mejor legibilidad y pruebas
Widget build(BuildContext context) {
  return Column(
    children: const [
      Header(),
      Expanded(child: ItemsList()),
      Footer(),
    ],
  );
}
class Header extends StatelessWidget {
  const Header();
  @override
  Widget build(BuildContext context) {
    return Text('Encabezado');
  }
}

Future Fuera de build()

FutureBuilder con future: fetchData() en build() lanza solicitudes en cada reconstrucción.

late Future dataFuture;
@override
void initState() {
  super.initState();
  dataFuture = fetchData();
}
Widget build(BuildContext context) {
  return FutureBuilder(
    future: dataFuture,
    builder: (_, snapshot) {
      if (!snapshot.hasData) return CircularProgressIndicator();
      return Text(snapshot.data.toString());
    },
  );
}

Future se crea una vez; la interfaz reacciona a los cambios.

Conclusiones Clave

  • Localizar actualizaciones: Usa ValueListenableBuilder, AnimatedBuilder en lugar de setState global.
  • const en todas partes: Reduce asignaciones en un 50–70% en interfaces complejas.
  • Lógica en initState: build() es solo para descripción declarativa.
  • ListView.builder + claves: Para listas dinámicas estables.
  • Composición de widgets: StatelessWidgets pequeños sobre monolitos.

— Editorial Team

Advertisement 728x90

Leer después