DEV Community

Julio César Pérez Ortiz
Julio César Pérez Ortiz

Posted on

Object Pooling en Flutter: Cuando Reutilizar es Mejor que Recrear

Renderizar 600 elementos animados a 60 FPS en Flutter parece imposible, hasta que cambias tu enfoque: deja de crear y destruir widgets, empieza a reutilizarlos.

El Patrón: Object Pooling

Object Pooling es un patrón de diseño donde mantienes un conjunto fijo de objetos reutilizables en lugar de crearlos y destruirlos constantemente. En Flutter, esto significa:

Enfoque tradicional (ineficiente):

// Cada frame destruyes y recreas widgets
Widget build() {
  return Stack(
    children: List.generate(count, (i) => createNewParticle())
  );
}
Enter fullscreen mode Exit fullscreen mode

Enfoque con pooling (eficiente):

// Creas una vez, reutilizas infinitamente
void initState() {
  particles = List.generate(count, (i) => Particle());
}

void update() {
  if (particle.isOffScreen) {
    particle.reset(); // Reutilizar, no destruir
  }
}
Enter fullscreen mode Exit fullscreen mode

Caso Real: Sistema de Lluvia

App del clima implementando Object Pooling

Para ilustrar este patrón, implementé un sistema de lluvia con 4 niveles de intensidad (100 a 600 gotas):

class _RainDropState extends State<_RainDrop> with SingleTickerProviderStateMixin {
  late double dx, dy, vy;
  late final Ticker _ticker;

  @override
  void initState() {
    super.initState();
    _randomizeValues();
    _ticker = createTicker((elapsed) {
      setState(() {
        dy += vy;
        if (dy >= widget.screenHeight + 100) {
          _randomizeValues(); // Reset, no destruir
        }
      });
    });
    _ticker.start();
  }

  void _randomizeValues() {
    dx = random.nextDouble() * widget.screenWidth;
    dy = -500 - (random.nextDouble() * -500);
    vy = rangeMap(z, 0, 20, minSpeed, maxSpeed);
  }
}
Enter fullscreen mode Exit fullscreen mode

¿Por Qué Funciona?

Eliminación de presión sobre el GC(Garbage Collector): No hay objetos temporales que limpiar.

Memoria predecible: La cantidad de widgets nunca cambia durante la ejecución.

CPU constante: El costo de actualizar es siempre el mismo, no importa cuánto tiempo pase.

Cache-friendly: Los objetos permanecen en memoria, mejor localidad de datos.

Más Allá de las Partículas: Patrones Relacionados

1. Flyweight Pattern: Compartir Datos Inmutables

En lugar de duplicar datos constantes, compártelos:

enum RainLevel {
  ligera(count: 100, farSpeed: 10, nearSpeed: 4),
  moderada(count: 250, farSpeed: 15, nearSpeed: 6),
  torrencial(count: 600, farSpeed: 30, nearSpeed: 12);

  final int count;
  final double farSpeed;
  final double nearSpeed;

  const RainLevel({
    required this.count,
    required this.farSpeed,
    required this.nearSpeed,
  });
}
Enter fullscreen mode Exit fullscreen mode

Cada gota accede a las mismas constantes en lugar de copiarlas.

2. Dirty Flag Pattern: Actualizar Solo lo Necesario

No reconstruyas todo el árbol de widgets si solo cambió un elemento:

AnimatedContainer(
  duration: const Duration(seconds: 2),
  curve: Curves.easeInOut,
  decoration: BoxDecoration(gradient: currentGradient),
)
Enter fullscreen mode Exit fullscreen mode

AnimatedContainer solo interpola entre estados, no reconstruye el widget completo.

3. State Pattern: Comportamientos Intercambiables

Los gradientes de fondo son constantes precalculadas:

class ColorPalette {
  static const Gradient dayGradient = LinearGradient(
    colors: [skyBlue, lightSkyBlue],
  );

  static const Gradient nightGradient = LinearGradient(
    colors: [midnightBlue, deepDarkBlue],
  );
}

Gradient get backgroundGradient {
  if (currentWeather == WeatherCondition.lluvioso) {
    return ColorPalette.rainyGradient;
  }
  return _getTimeBasedGradient();
}
Enter fullscreen mode Exit fullscreen mode

Cambiar de estado solo cambia la referencia, no recalcula nada.

4. Command Pattern: Animaciones Reutilizables

Una sola animación controla múltiples elementos:

_controller = AnimationController(
  vsync: this,
  duration: Duration(minutes: 5),
);
_controller.repeat();

// Usada por sol Y luna simultáneamente
CelestialPositions _calculatePositions(double progress) {
  final angle = (progress * 2 * pi) - (pi / 2);
  return CelestialPositions(
    sunPosition: Offset(center.dx + radius * cos(angle)),
    moonPosition: Offset(center.dx + radius * cos(angle + pi)),
  );
}
Enter fullscreen mode Exit fullscreen mode

Aplicaciones Más Allá del Clima

Este enfoque arquitectónico aplica a cualquier sistema con elementos múltiples y repetitivos:

Juegos

  • Bullet pools: Reutilizar proyectiles en shooters
  • Enemy spawners: Pool de enemigos que se reactivan
  • Particle effects: Explosiones, humo, chispas

Visualización de Datos

  • Real-time charts: Actualizar puntos sin recrear el gráfico
  • Audio visualizers: Barras de ecualizador que cambian altura
  • Network graphs: Nodos que actualizan posición

E-commerce

  • Infinite scroll: Reutilizar cards al hacer scroll
  • Carousels: Pool de items que rotan
  • Loading skeletons: Placeholders que pulsan

Productividad

  • Toast notifications: Pool de mensajes flotantes
  • Drag & drop: Cursores y previsualizaciones reutilizables
  • Canvas tools: Pinceles, formas, selecciones

El Principio Fundamental

Separar identidad de estado:

  • La identidad (el widget/objeto) permanece constante
  • El estado (posición, color, tamaño) cambia continuamente
// MAL: Mezclas identidad con estado
List<Widget> buildItems() {
  return data.map((item) => ItemWidget(item)).toList();
}

// BIEN: Identidad fija, estado mutable
List<ItemWidget> items = List.generate(count, (i) => ItemWidget());
void updateItems(List<Data> newData) {
  for (int i = 0; i < items.length; i++) {
    items[i].updateData(newData[i]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Cuándo Aplicar Este Patrón

Úsalo cuando:

  • Tienes >50 elementos similares
  • Los elementos aparecen/desaparecen frecuentemente
  • El rendimiento es crítico (60 FPS)
  • La estructura es repetitiva pero el contenido varía

Evítalo cuando:

  • Tienes pocos elementos (<10)
  • La estructura cambia radicalmente
  • La lógica de reseteo es más compleja que recrear
  • El costo de mantener el pool supera el beneficio

Resultados Medibles

Antes (recreación constante):

  • 100 elementos: 30 FPS
  • 250 elementos: 15 FPS
  • Picos de GC cada 2-3 segundos

Después (object pooling):

  • 600 elementos: 60 FPS constante
  • Sin picos de GC perceptibles
  • Uso de memoria estable

Conclusión

Object Pooling no es solo una optimización, es un cambio de mentalidad: piensa en tus widgets como actores reutilizables en un escenario, no como decorados desechables.

La próxima vez que necesites animar muchos elementos, pregúntate: ¿realmente necesito crear y destruir, o puedo simplemente resetear y reutilizar?

Proyecto Completo

El código completo de la aplicación de clima está disponible en GitHub:
App del Clima

Características implementadas:

  • Object pooling para 600+ gotas de lluvia simultáneas
  • 4 niveles de intensidad de lluvia (ligera, moderada, fuerte, torrencial)
  • Sistema de transiciones suaves con AnimatedContainer
  • Rotación celestial (sol/luna) con un solo AnimationController
  • Integración con API REST para datos climáticos reales
  • Arquitectura MVVM limpia y escalable
  • Pronósticos por hora y diarios interactivos

¿Has aplicado object pooling en tus proyectos Flutter? ¿Qué otros patrones de optimización has encontrado útiles? Comparte tu experiencia en los comentarios.

Top comments (0)