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())
);
}
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
}
}
Caso Real: Sistema de Lluvia
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);
}
}
¿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,
});
}
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),
)
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();
}
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)),
);
}
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]);
}
}
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)