Diseña una librería de carga de imágenes desde cero (piensa: Glide/Coil/SDWebImage)
Esboza el pipeline de peticiones —caché en memoria, caché en disco, red, decodificación y cancelación consciente del ciclo de vida— que corre en producción cualquier cargador de imágenes real.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña una librería de carga de imágenes desde cero (piensa: Glide/Coil/SDWebImage)
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Aclarando los requisitos
Una librería de carga de imágenes suena como un problema resuelto —"descarga una URL, muestra un bitmap"— pero la razón por la que Glide, Coil y SDWebImage llegan cada una a decenas de miles de líneas es que los requisitos reales giran todos en torno a recursos escasos bajo alta rotación: un RecyclerView/LazyColumn puede vincular y desvincular cincuenta peticiones de imagen por segundo mientras se hace scroll, la memoria es tan limitada que un puñado de bitmaps a resolución completa puede provocar un OOM en la app, y cada petición necesita cancelarse en el instante en que su vista desaparece, o si no es CPU y ancho de banda desperdiciados. Así que enmarcaría los requisitos así: (1) nunca bloquear el hilo principal —ni para buscar en caché, ni para decodificar, ni para nada—; (2) nunca mantener un bitmap más grande de lo que el ImageView destino realmente necesita; (3) cancelar automáticamente el trabajo ligado a una vista/lifecycle destruido; (4) cachear de forma agresiva en dos niveles (memoria y disco) para que volver a hacer scroll o reabrir una pantalla sea instantáneo; (5) transformar imágenes (redimensionar, recortar, difuminar) como un paso componible del pipeline, no algo ad hoc. También confirmaría si se necesita soporte de animación GIF/WebP y UX de placeholder/error/crossfade, ya que eso condiciona la API pública aunque no cambie la forma del pipeline central.
#El pipeline de la petición
El modelo mental sobre el que diseñaría es una única petición fluyendo a través de una cadena ordenada de resolutores, cada uno de los cuales puede o bien satisfacer la petición de inmediato o pasarla a la siguiente etapa: caché de memoria → caché de disco → descarga de red → decodificación → transformación → escritura en caché de memoria → destino. La idea clave de diseño es que cada etapa posterior a "revisar la caché de memoria" ocurre fuera del hilo principal, y el pipeline se indexa no solo por URL sino por una clave compuesta que incluye el tamaño solicitado y cualquier transformación, porque una imagen fuente de 1200px redimensionada a una miniatura de 48px y la misma redimensionada a una imagen hero de 400px son entradas de caché distintas —cachear solo por URL es el error de principiante más común en este espacio, y anula en silencio todo el propósito del downsampling.
#Caché: dos niveles, dos políticas muy distintas
La caché de memoria es un LRU indexado por la clave compuesta de la petición, dimensionada como una fracción de la memoria disponible de la app (una heurística común es aproximadamente un octavo del heap máximo, ajustado por clase de dispositivo), que guarda bitmaps ya decodificados y listos para dibujar. El desalojo tiene que ser consciente del tamaño, no del conteo — una caché que guarda veinte bitmaps de wallpaper en 4K y una caché que guarda veinte miniaturas de 48px tienen huellas de memoria radicalmente distintas, así que la función de "tamaño" del LRU devuelve cantidad de bytes, no cantidad de entradas. La caché de disco guarda los bytes originales, sin decodificar (no el bitmap redimensionado) indexados solo por URL, porque la respuesta cruda de red es reutilizable para cualquier variante de tamaño/transformación que un llamador pida más adelante — volver a decodificar desde un archivo cacheado sigue siendo mucho más barato que volver a descargar de la red. Dimensionaría la caché de disco como un LRU con tope en disco (decenas hasta un par de cientos de MB según la app) con el clásico journal estilo DiskLruCache para llevar registro de desalojo a prueba de crashes.
#La decodificación es la parte que realmente provoca crashes
Llamar ingenuamente a BitmapFactory.decodeStream (Android) o UIImage(data:) (iOS) sobre un JPEG crudo de 12MP destinado a un avatar de 64dp asigna un bitmap decenas de veces más grande de lo que la pantalla puede siquiera mostrar, y hacer esto por cada celda de una lista con scroll es la causa clásica de OutOfMemoryError/terminaciones por presión de memoria. El pipeline tiene que inspeccionar primero los límites de la imagen (inJustDecodeBounds en Android, o leer las propiedades del CGImage source en iOS) sin asignar datos de píxeles, calcular un factor de downsampling a partir del tamaño medido de la vista destino, y solo entonces decodificar a esa resolución reducida (inSampleSize, o CGImageSourceCreateThumbnailAtIndex con un kCGImageSourceThumbnailMaxPixelSize). Este único paso —decodificar-al-tamaño-destino en lugar de decodificar-y-luego-escalar— suele ser la mayor ganancia de rendimiento y memoria de todo el sistema, mayor que cualquiera de los dos niveles de caché.
#Cancelación consciente del lifecycle y deduplicación de peticiones
Dos cosas distinguen un cargador de juguete de uno de producción: la cancelación y la deduplicación. Cada petición se etiqueta a la vista destino y, idealmente, a un observador del lifecycle del llamador (una Activity/Fragment/LocalLifecycleOwner de Compose, o el deinit de un UIViewController vía una referencia débil); cuando ese lifecycle llega a destroy, o cuando la vista está a punto de reutilizarse para una URL distinta (el clásico bug de RecyclerView donde "la celda reciclada muestra la imagen equivocada"), la petición en vuelo se cancela y su trabajo se descarta antes de que pueda vincular un bitmap obsoleto. La deduplicación significa que si la misma clave se solicita dos veces de forma concurrente —dos celdas mostrando brevemente el mismo avatar durante un scroll rápido— el pipeline adjunta ambos callbacks a un único Job/Task en vuelo en lugar de hacer la descarga y decodificación dos veces.
// Un boceto recortado de la API pública — suficiente superficie para discutir, no una implementación completa.interface ImageLoader {fun load(url: String): RequestBuilder}interface RequestBuilder {fun size(width: Int, height: Int): RequestBuilderfun placeholder(@DrawableRes res: Int): RequestBuilderfun error(@DrawableRes res: Int): RequestBuilderfun transform(vararg transformations: Transformation): RequestBuilderfun crossfade(durationMs: Int = 200): RequestBuilderfun into(target: ImageView): Disposablesuspend fun await(): Bitmap}interface Transformation {fun key(): String // participa en la clave de la caché de memoriafun transform(input: Bitmap): Bitmap}interface Disposable {fun dispose() // cancela el trabajo en vuelo, se desvincula del target}// Forma interna: una petición es dedup'd y cancelable por construcción.internal class ImageRequest(val key: String, // url + tamaño + firma de transformval target: WeakReference<ImageView>,val lifecycle: Lifecycle?,val job: Deferred<Bitmap>, // compartido entre llamadores dedup'd)
#Capa de red y priorización
La etapa de red se sitúa detrás de una interfaz (para poder respaldarse con OkHttp, URLSession, o lo que sea que la app anfitriona ya use) y soporta coalescencia de peticiones — la misma URL solicitada por cinco vistas distintas resulta en una sola llamada HTTP. También añadiría prioridad: las imágenes visibles actualmente en pantalla deberían tener preferencia sobre las imágenes precargadas para contenido al que el usuario aún no ha hecho scroll, lo cual significa que el executor subyacente es una cola de prioridad, no un simple pool de hilos, y las peticiones visibles-vs-precarga llevan prioridades distintas que un listener de scroll ajusta a medida que los ítems entran/salen del viewport.
#Tradeoffs que señalaría
El mayor tradeoff es el tamaño de la caché de memoria contra el costo de decodificación: una caché más grande significa menos decodificaciones redundantes pero más presión hacia el OOM en dispositivos de gama baja, así que haría que el tamaño de la caché se adapte a la clase de memoria del dispositivo en lugar de ser una constante fija. Otro es la política de desalojo de la caché de disco — el LRU puro es simple pero no tiene en cuenta la varianza de tamaño de las imágenes, así que una imagen hero grande puede desalojar a una docena de miniaturas pequeñas; un LRU ponderado por tamaño es más correcto pero añade costo de contabilidad. Finalmente, señalaría que todo este diseño asume imágenes ráster basadas en bitmap; soportar formatos vectoriales o miniaturas de video necesitaría estrategias de decodificación adicionales, pero encajarían en la misma forma de pipeline en la etapa de decodificación, lo cual es una buena señal de que la arquitectura está descompuesta en las costuras correctas.