Imágenes, cámara y transferencia de archivos en Android
Coil, Photo Picker, FileProvider, compresión, EXIF, multipart y estado guardado
Introducción
Una aplicación puede mostrar imágenes recibidas por red, permitir que el usuario seleccione una fotografía existente o capturar una imagen nueva. Aunque las tres operaciones parecen similares desde la interfaz, involucran recursos y responsabilidades diferentes: comunicación HTTP, memoria, permisos temporales, procesos de otras aplicaciones, almacenamiento y privacidad.
Esta guía estudia el recorrido completo de una imagen en Android con Kotlin y Jetpack Compose: obtener una imagen remota, seleccionarla o capturarla, prepararla para transferencia, subirla a un servidor y conservar las referencias necesarias ante la recreación de la actividad o el proceso. Los ejemplos emplean un catálogo de reportes con fotografías, pero los conceptos son aplicables a perfiles, productos, evidencias o publicaciones.
Al terminar el estudio, deberías poder explicar por qué una imagen comprimida ocupa mucha más memoria al decodificarse, cómo funcionan las URI content://, cuándo se necesita FileProvider, qué información contiene EXIF y qué estado se debe restaurar si Android termina un proceso.
Lo imprescindible para el examen
Explicar la URI y permisos temporales; seleccionar y capturar fotos; estimar memoria de bitmaps; procesar EXIF, multipart y estado guardado; analizar privacidad y caché.
Se evalúa la capacidad de explicar, interpretar fragmentos y justificar decisiones. Las versiones puntuales de librerías, URLs, nombres de proyecto y comandos accesorios quedan como contexto de consulta.
Criterios transversales para interpretar esta guía
Principio transferible. Conviene distinguir la responsabilidad de una tecnología de la forma concreta en que se utilizó en la práctica. Un ejemplo de ViewModel, Retrofit, Hilt o WebSocket ilustra una solución posible; su configuración exacta no es una regla universal.
Decisión del proyecto. Los nombres, rutas, versiones, intervalos, tamaños y elecciones de librerías del caso original sirven para interpretar el código. Salvo indicación expresa, no se exige memorizarlos para el examen.
Alcance del código. Los fragmentos son recursos didácticos; pueden omitir imports, dependencias, permisos, detalles de manejo de errores o infraestructura. Comprender un fragmento no equivale a comprobar que compila como proyecto independiente.
Referencias cruzadas. La síntesis transversal de la colección reúne las distinciones entre estado y evento, persistencia, reintentos, idempotencia y límites de las pruebas.
1. El ciclo de vida de una imagen en una aplicación
No todas las imágenes tienen el mismo origen ni requieren el mismo tratamiento. Considera un sistema de reportes en el que los usuarios adjuntan una fotografía de un desperfecto:
- Visualización: la lista descarga y presenta miniaturas remotas.
- Selección: una persona elige una fotografía desde el selector de imágenes.
- Captura: alternativamente, abre una aplicación de cámara que guarda una fotografía en una URI preparada por la app.
- Preparación: se decodifica de forma controlada, se ajusta la orientación, se reduce el tamaño y se genera un archivo nuevo.
- Transferencia: el cliente envía un cuerpo HTTP multipart; el servidor devuelve un identificador.
- Publicación: se crea el reporte con ese identificador, sin enviar de nuevo los bytes de la fotografía.
Es importante distinguir tres representaciones:
| Representación | Ejemplo | Qué representa |
|---|---|---|
| URI local | content://.../foto/17 |
Acceso a contenido administrado por un proveedor |
| Bytes | JPEG generado después de comprimir | Contenido binario que se enviará |
| Identificador remoto | d8f2...jpg |
Referencia que devuelve el almacén de imágenes |
Un error de diseño frecuente es utilizar indistintamente una ruta local, una URL y el identificador que devuelve el servidor. Cada dato cumple una función distinta.
2. Mostrar imágenes remotas con Coil
Una imagen remota no debería descargarse y decodificarse manualmente desde cada Composable. Una biblioteca como Coil organiza la descarga, la decodificación, el redimensionamiento, la caché y la presentación en Compose.
2.1. Un componente de imagen con espacio reservado
@Composable
fun FotoReporte(
imagen: Any?,
modifier: Modifier = Modifier
) {
if (imagen != null) {
AsyncImage(
model = imagen,
contentDescription = "Fotografía del reporte",
contentScale = ContentScale.Crop,
modifier = modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
)
}
}
model puede recibir una URL remota o una URI local admitida por los componentes de Coil. ContentScale.Crop llena el área disponible, aunque podría recortar parte de la foto. aspectRatio(16f / 9f) reserva dimensiones antes de que termine la descarga, evitando que las tarjetas cambien bruscamente de tamaño.
Una imagen decorativa puede usar contentDescription = null. Una imagen que aporta información necesita una descripción útil, acorde con lo que se conoce sobre ella; un texto genérico no siempre ofrece suficiente accesibilidad.
2.2. Descargar contenido protegido
Si las imágenes requieren autenticación, el cargador debe enviar los mismos encabezados que las demás peticiones protegidas. En el proyecto original se configuró Coil con el cliente OkHttp que ya usaban Retrofit y el mecanismo de renovación del token:
@Module
@InstallIn(SingletonComponent::class)
object ImagenesModule {
@Provides
@Singleton
fun proporcionarImageLoader(
@ApplicationContext context: Context,
cliente: OkHttpClient
): ImageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { cliente }))
}
.crossfade(true)
.build()
}
Este fragmento ilustra la configuración de un ImageLoader mediante Hilt. Para que toda la interfaz lo utilice, debe integrarse como el cargador compartido de la aplicación, mediante la configuración correspondiente de Coil.
Compartir un OkHttpClient configurado permite reutilizar interceptores, políticas de autenticación, registros y conexiones. No significa que cada solicitud tenga que utilizar idénticas opciones: una conexión de larga duración puede necesitar otra configuración de tiempo de espera.
2.3. La caché puede esconder errores
Supongamos que una miniatura protegida se descargó mientras la sesión era válida. Después se elimina, por error, el interceptor que adjunta el token. La miniatura podría seguir apareciendo porque Coil la recupera de su caché y no hace una petición nueva.
Conclusión para el diagnóstico: ver una imagen en pantalla no demuestra que la comunicación HTTP esté funcionando. Para comprobar una falla de autenticación, utiliza una imagen no descargada previamente, revisa los registros adecuados o invalida de forma controlada la caché.
La caché también exige una decisión de privacidad. Si se almacenan imágenes de acceso restringido, hay que evaluar su persistencia, la separación entre usuarios y la invalidación cuando cambian los permisos. La cabecera Cache-Control: immutable solo resulta apropiada para recursos cuyo contenido verdaderamente no cambia bajo la misma dirección.
3. Comprender las URI de contenido
Una URI es un identificador de recurso. En Android, una referencia content:// identifica contenido ofrecido a través de un ContentProvider. No debe tratarse como una ruta tradicional de archivo.
content://com.ejemplo.proveedor/imagenes/17
Una aplicación recibe la URI y accede al contenido mediante ContentResolver:
fun leerBytes(context: Context, uri: Uri): ByteArray? =
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
Este ejemplo es apropiado para un recurso pequeño y controlado. No conviene usar readBytes() indiscriminadamente sobre fotografías grandes, porque carga el archivo completo en memoria. En el procesamiento de imágenes se prefieren flujos y decodificación redimensionada.
El acceso a la URI depende de los permisos concedidos por el proveedor y por el mecanismo de selección. Tener el texto de una URI guardado no garantiza poder leer el recurso indefinidamente.
3.1. Photo Picker
Para permitir que el usuario seleccione una imagen, Android proporciona un selector que concede acceso a los elementos elegidos sin otorgar acceso general a toda la galería.
@Composable
fun SelectorDeFoto(onSeleccionar: (Uri) -> Unit) {
val selector = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia()
) { uri ->
if (uri != null) onSeleccionar(uri)
}
Button(onClick = {
selector.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
}) {
Text("Elegir fotografía")
}
}
Este mecanismo evita solicitar permisos amplios de lectura de imágenes para la selección realizada por el usuario. Si se cancela la operación, el callback recibe null.
Precisión: la duración del acceso no debe suponerse universalmente igual a la vida de una Activity. Depende del contrato, del proveedor y de la posibilidad de conservar permisos cuando corresponda. Si la imagen se usará mucho después, una estrategia fiable consiste en copiarla a almacenamiento privado cuando todavía se tiene acceso o gestionar formalmente la persistencia del permiso, según el mecanismo utilizado.
4. Capturar una fotografía mediante otra aplicación
El contrato ActivityResultContracts.TakePicture() permite solicitar una fotografía a una aplicación de cámara. La app que solicita la captura proporciona primero la URI donde se escribirá el resultado.
4.1. Por qué hace falta FileProvider
La cámara es otro proceso y no puede acceder libremente a los archivos privados de nuestra aplicación. FileProvider permite representar un archivo autorizado mediante una URI content:// y conceder a la cámara acceso limitado a ese recurso.
Un esquema conceptual:
Aplicación A
└─ crea un archivo privado para la foto
└─ obtiene URI con FileProvider
└─ lanza TakePicture(uri)
│
▼
Aplicación de cámara
└─ escribe el contenido autorizado
│
▼
Aplicación A
└─ recibe true / false
└─ procesa la foto desde la misma URI
En el manifiesto se declara el proveedor con su autoridad, sin exportarlo públicamente y con concesión de permisos URI:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
El XML file_paths limita qué directorios se pueden compartir. No se debería configurar una ruta más amplia de la necesaria.
4.2. Un lanzador de cámara y su resultado
@Composable
fun CapturarFoto(
crearUri: () -> Uri,
onFotoTomada: (Uri) -> Unit
) {
var uriPendiente by rememberSaveable {
mutableStateOf<Uri?>(null)
}
val camara = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture()
) { exito ->
val uri = uriPendiente
if (exito && uri != null) onFotoTomada(uri)
uriPendiente = null
}
Button(onClick = {
val uri = crearUri()
uriPendiente = uri
camara.launch(uri)
}) {
Text("Tomar fotografía")
}
}
Este ejemplo supone que crearUri() crea previamente un archivo válido y lo expone con FileProvider. Su implementación y la configuración del proveedor forman parte de la infraestructura de archivos de la aplicación.
Con TakePicture(), cuando la captura se delega a otra aplicación, normalmente no hace falta solicitar el permiso CAMERA para ese flujo. Si la propia app controla el hardware de cámara, por ejemplo usando CameraX, la situación es diferente y sí requiere gestionar ese permiso.
4.3. ¿Dónde guardar el archivo temporal?
cacheDircontiene archivos que el sistema puede eliminar para recuperar espacio; no es garantía de persistencia.filesDirconserva archivos privados hasta que la app los elimine, el usuario borre sus datos o desinstale la aplicación.
Si la fotografía debe permanecer disponible durante el proceso de edición y publicación, filesDir ofrece mayor estabilidad. A cambio, la app debe eliminar los archivos que ya no necesita. Elegir almacenamiento estable no elimina la obligación de administrar espacio.
5. Memoria: tamaño del archivo frente a tamaño del bitmap
Un JPEG contiene píxeles comprimidos. Al decodificarlo, la imagen ocupa una cantidad de memoria vinculada principalmente a sus dimensiones y al formato de píxel, no al tamaño del archivo comprimido.
Para un bitmap típico de cuatro bytes por píxel:
[ \text{memoria aproximada} = \text{ancho} \times \text{alto} \times 4 ]
| Dimensiones | Píxeles | Memoria aproximada |
|---|---|---|
| 4 000 × 3 000 | 12 000 000 | 48 MB |
| 2 000 × 1 500 | 3 000 000 | 12 MB |
| 1 000 × 750 | 750 000 | 3 MB |
Son valores aproximados y dependen de la configuración interna del bitmap. La enseñanza principal es que un JPEG de pocos megabytes puede convertirse en decenas de megabytes de memoria al abrirlo.
5.1. Consultar dimensiones sin decodificar todos los píxeles
val opciones = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream, null, opciones)
}
val ancho = opciones.outWidth
val alto = opciones.outHeight
inJustDecodeBounds obtiene información de dimensiones sin crear el bitmap de tamaño completo. A continuación se decide cuánto reducir antes de decodificar.
5.2. Reducir desde el inicio con inSampleSize
var muestra = 1
val ladoMayor = max(ancho, alto)
while (ladoMayor / (muestra * 2) >= 1280) {
muestra *= 2
}
val opcionesLectura = BitmapFactory.Options().apply {
inSampleSize = muestra
}
val bitmap = context.contentResolver.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, opcionesLectura)
} ?: error("No se pudo decodificar la imagen")
Cuando inSampleSize = 2, el decodificador reduce aproximadamente cada dimensión a la mitad y el número de píxeles a una cuarta parte. Con inSampleSize = 4, el consumo de memoria correspondiente a los píxeles suele acercarse a una dieciseisava parte.
Para cumplir un tamaño máximo exacto, después se aplica una escala adicional si hace falta. Esta operación puede crear otro bitmap, de modo que el pico de memoria puede superar el tamaño de la imagen final.
5.3. Hilos y corrutinas
La lectura, la decodificación y la escritura de archivos pueden tardar. La práctica sitúa el procesamiento fuera del hilo principal:
suspend fun prepararImagen(uri: Uri): ByteArray =
withContext(Dispatchers.IO) {
// Abrir el contenido, decodificar reducido,
// corregir la orientación y generar un JPEG.
TODO("Implementar procesamiento")
}
Dispatchers.IO se emplea aquí para las operaciones de entrada/salida. Si una transformación intensiva de píxeles domina el trabajo, también puede justificarse Dispatchers.Default para el tramo estrictamente computacional. Una función suspend por sí sola no garantiza que el procesamiento sea ligero para el hilo que la ejecuta.
6. EXIF: orientación y privacidad
EXIF es un conjunto de metadatos que puede acompañar a una fotografía. Según el dispositivo y la configuración, puede incluir orientación, fecha, modelo de cámara y coordenadas GPS.
Orientación. Algunos dispositivos guardan los píxeles con una orientación distinta de la visual y registran una instrucción para girarlos. Por eso una fotografía puede verse correctamente en la galería y, tras un procesamiento que ignore EXIF, aparecer rotada.
Privacidad. Publicar el archivo original podría revelar metadatos que el usuario no esperaba compartir, en particular información de ubicación.
Un procesamiento coherente:
- Leer la orientación con
ExifInterface. - Decodificar con el tamaño apropiado.
- Aplicar la transformación necesaria a los píxeles.
- Crear un JPEG nuevo con
Bitmap.compress(...). - Comprobar el resultado y, cuando sea necesario, verificar que no se exportaron metadatos sensibles.
La práctica utiliza el hecho de que Bitmap.compress no copia automáticamente el bloque EXIF original. Esto permite generar una nueva imagen sin los metadatos previos. Precisión técnica: las orientaciones EXIF incluyen casos de reflejo o transposición, además de los giros de 90°, 180° y 270°. Una implementación completa debe contemplarlos o usar una transformación que los resuelva.
val orientacion = context.contentResolver.openInputStream(uri)?.use {
ExifInterface(it).getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
} ?: ExifInterface.ORIENTATION_NORMAL
Además de la privacidad, reducir y recomprimir una imagen puede cambiar su calidad visual. La calidad JPEG elegida debe equilibrar detalle útil, tamaño y requisitos del servidor. Recomprimir al 100 % no implica conservar la calidad original ni producir el archivo más pequeño.
7. Subida de imágenes con Retrofit y multipart
En el ejemplo de la práctica, el servidor ofrece dos operaciones distintas:
POST /imagenes → recibe bytes y devuelve una clave
POST /reportes → recibe datos del reporte y la clave
El servidor puede almacenar los bytes en un sistema de objetos y guardar únicamente la referencia en una base relacional. Es una opción arquitectónica frecuente; no es una imposibilidad técnica almacenar imágenes en una base de datos. La elección depende de tamaño, volumen, consistencia, costos y políticas de acceso.
7.1. Declarar el endpoint
interface ReportesApi {
@Multipart
@POST("imagenes")
suspend fun subirImagen(
@Part archivo: MultipartBody.Part
): ImagenSubidaDto
@POST("reportes")
suspend fun crearReporte(
@Body reporte: NuevoReporteDto
): ReporteDto
}
@Serializable
data class ImagenSubidaDto(
val id: String,
val tipo: String,
val bytes: Int
)
Las anotaciones @Multipart y @Part indican que el cuerpo de la solicitud contiene partes con nombre y contenido binario. El nombre del campo debe coincidir con el contrato del servidor.
7.2. Construir la parte
suspend fun subir(uri: Uri): String {
val jpeg: ByteArray = compresor.comprimir(uri)
val parte = MultipartBody.Part.createFormData(
"archivo", // nombre exigido por la API
"evidencia.jpg", // nombre del archivo enviado
jpeg.toRequestBody("image/jpeg".toMediaType())
)
return api.subirImagen(parte).id
}
Este ejemplo mantiene los bytes en memoria. Para archivos grandes puede ser más conveniente un RequestBody que transmita desde un flujo o archivo temporal sin construir un gran ByteArray.
7.3. Coordinar las dos peticiones
La publicación no es una sola operación de red:
Seleccionar/capturar
↓
Preparar JPEG
↓
POST /imagenes ──> recibe clave
↓
POST /reportes con esa clave
↓
Confirmación y navegación
Si falla la primera petición, el reporte no debe enviarse con una imagen que no existe. Si la imagen se sube correctamente, pero falla la publicación del reporte, puede quedar un objeto huérfano en el almacenamiento remoto. Una aplicación real necesita decidir si lo elimina, si caduca automáticamente o si conserva el identificador para reintentar sin subir otra vez.
También debe contemplarse que un reintento después de una interrupción puede duplicar operaciones. Cuando el servicio lo admita, los identificadores de operación o claves de idempotencia permiten reducir ese riesgo.
8. Estado de publicación y errores
Un envío con imagen tiene varias etapas que conviene comunicar al usuario:
data class PublicarUiState(
val titulo: String = "",
val descripcion: String = "",
val imagen: Uri? = null,
val etapa: String? = null,
val error: String? = null
) {
val enviando: Boolean get() = etapa != null
val puedePublicar: Boolean get() =
titulo.isNotBlank() && descripcion.isNotBlank() && !enviando
}
etapa puede representar «Preparando imagen», «Subiendo imagen» o «Publicando reporte». Mientras existe, se evita iniciar otra publicación mediante el botón. El ViewModel coordina la operación dentro de viewModelScope y navega solo después de recibir confirmación del servidor.
fun publicar(alConfirmar: () -> Unit) {
if (!uiState.puedePublicar) return
viewModelScope.launch {
try {
uiState = uiState.copy(etapa = "Preparando imagen", error = null)
val clave = uiState.imagen?.let { imagenes.subir(it) }
uiState = uiState.copy(etapa = "Publicando reporte")
reportes.publicar(uiState.titulo, uiState.descripcion, clave)
uiState = uiState.copy(etapa = null)
alConfirmar()
} catch (e: ImagenIlegible) {
uiState = uiState.copy(etapa = null, error = "No se pudo leer la imagen")
} catch (e: IOException) {
uiState = uiState.copy(etapa = null, error = "No se completó la comunicación")
} catch (e: HttpException) {
uiState = uiState.copy(etapa = null, error = "El servidor respondió ${e.code()}")
}
}
}
Este ejemplo supone que ImagenIlegible deriva de IOException; de ahí la importancia de atraparla antes de IOException. HttpException se trata por separado porque representa una respuesta HTTP no exitosa, aunque debe distinguirse entre códigos permanentes y recuperables. Si se añade un catch general, hay que propagar CancellationException, ya que la cancelación de una corrutina no debe convertirse en un error ordinario de interfaz.
En el contrato usado en la práctica, un archivo que supera el límite puede producir HTTP 413 y un campo multipart mal nombrado puede provocar HTTP 422. Son respuestas de ejemplo del servidor, no reglas sobre todas las API.
9. Recuperar el estado cuando Android termina el proceso
Abrir la cámara implica pasar a otra aplicación. La nuestra puede quedar en segundo plano y Android puede terminar su proceso para recuperar memoria. Al regresar, la actividad y el ViewModel pueden recrearse.
Hay dos problemas distintos: recuperar la URI de salida de la cámara y recuperar los campos que el usuario ya escribió.
| Mecanismo | Recomposición | Cambio de configuración | Recreación tras muerte del proceso* |
|---|---|---|---|
remember |
Sí | No | No |
ViewModel |
Sí | Sí | No |
rememberSaveable |
Sí | Sí | Sí, en las condiciones del estado guardado |
SavedStateHandle |
Sí | Sí | Sí, en las condiciones del estado guardado |
| Archivo privado persistente | Sí | Sí | Sí, mientras no sea eliminado |
El estado guardado no equivale a persistencia permanente ni cubre todas las formas de cierre, eliminación o terminación. Su restauración depende de que Android haya conservado el estado de la tarea o actividad.
9.1. Qué guardar
rememberSaveable puede conservar la URI pendiente de la cámara; SavedStateHandle puede conservar el título, la descripción o una referencia pequeña a la imagen seleccionada. No se debe guardar un Bitmap grande: el mecanismo utiliza estado serializable limitado y puede causar errores como TransactionTooLargeException.
@HiltViewModel
class EditarReporteViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
var titulo: String
get() = savedStateHandle["titulo"] ?: ""
set(value) { savedStateHandle["titulo"] = value }
}
El estado «Subiendo…» no debería restaurarse sin comprobar la operación, porque una corrutina interrumpida no continúa automáticamente después de que el proceso se reinicia. Si la tarea debe sobrevivir a la muerte del proceso, requiere una estrategia explícita de persistencia y ejecución diferida, no únicamente SavedStateHandle.
9.2. Referencia guardada frente a acceso real
Restaurar una Uri no garantiza que el contenido continúe disponible. Para fotos creadas por la app y alojadas en su almacenamiento privado, debe conservarse el archivo. Para fotos obtenidas del selector, debe conservarse el acceso de la manera apropiada o copiar la imagen mientras exista permiso para leerla.
10. Seguridad, privacidad y entorno de desarrollo
Hay varias medidas que se relacionan con el tratamiento de imágenes:
- Minimización: no enviar una imagen de resolución mucho mayor de la necesaria y eliminar metadatos que no sean requeridos.
- Acceso: si una imagen pertenece a una cuenta, el servidor debe comprobar quién puede descargarla; ocultar una URL en la UI no la protege.
- Registros: evitar escribir tokens, rutas privadas y contenido sensible en los logs de producción.
- Validación del servidor: comprobar tipos y tamaños aceptados; no confiar únicamente en el tipo MIME o extensión que afirma el cliente.
- Archivos temporales: disponer de una política de limpieza y evitar conservar copias innecesarias.
Para conectar un emulador Android con un servidor que corre en la computadora anfitriona se utiliza habitualmente 10.0.2.2. En la práctica, el acceso HTTP sin cifrado se permite solo para desarrollo, mientras que una versión distribuida debe utilizar HTTPS y la configuración adecuada de seguridad de red.
Conexión transversal: alcance y decisiones
Persistencia, seguridad y secuencias de subida
rememberSaveable y SavedStateHandle conservan cantidades pequeñas de estado bajo determinadas condiciones de restauración; no son almacenamiento permanente de archivos ni una garantía tras cualquier tipo de cierre. Una Uri guardada puede perder el permiso de lectura si no se obtuvo persistencia de acceso válida o se copió el contenido a almacenamiento controlado por la app.
Subir una imagen y luego crear un registro son dos operaciones, no una transacción única: si la segunda falla, puede quedar un objeto huérfano; si la respuesta de creación se pierde, repetirla puede duplicar registros. Es necesario definir limpieza/expiración de objetos sin referencia y, cuando proceda, idempotencia del servidor (guía 04). Una imagen privada en la caché de Coil puede persistir después del logout si no se configura una política adecuada (guía 06).
11. Diagnóstico de problemas frecuentes
| Síntoma | Posible causa | Qué revisar |
|---|---|---|
| La imagen protegida aparece vacía y el servidor registra 401 | Coil utiliza un cliente sin autenticación | Configuración de ImageLoader, OkHttp e interceptores |
| La imagen sigue apareciendo tras romper la red | Se recupera desde caché | Repetir con una imagen nueva o invalidar caché |
@Part parameters can only be used with multipart encoding |
Falta @Multipart |
Definición del endpoint de Retrofit |
Couldn't find meta-data for provider |
FileProvider no está bien declarado |
Autoridad, manifiesto y XML de rutas |
| La cámara regresa sin una foto accesible | URI incorrecta o archivo eliminado | Preparación de la URI y directorio empleado |
| Foto rotada después de subirla | La compresión ignoró EXIF | Lectura y aplicación de orientación |
OutOfMemoryError al preparar una fotografía |
Se decodificó con dimensiones completas | inJustDecodeBounds, inSampleSize, escalado |
| Se pierde el texto al volver de la cámara | El ViewModel anterior se destruyó | SavedStateHandle y restauración real del proceso |
| La URI se restaura, pero no abre | No se conservó archivo o permiso | Ciclo de vida y estrategia de acceso al contenido |
| El servidor responde 413 | El cuerpo excede su límite | Tamaño final y reglas de subida |
| El servidor responde 422 | El cuerpo no cumple el contrato | Nombre de parte, campos y validaciones |
12. Vocabulario esencial
| Término | Significado |
|---|---|
| Coil | Biblioteca para cargar, decodificar, almacenar en caché y presentar imágenes |
ImageLoader |
Componente que configura cómo se cargan las imágenes |
URI content:// |
Referencia a contenido expuesto mediante un proveedor |
ContentResolver |
Interfaz para acceder al contenido de las URI |
| Photo Picker | Selector de medios que otorga acceso a los elementos elegidos |
FileProvider |
Proveedor que comparte archivos autorizados mediante URI |
TakePicture |
Contrato que pide a otra aplicación capturar una foto en una URI |
Bitmap |
Representación de píxeles en memoria |
inJustDecodeBounds |
Consulta dimensiones sin crear el bitmap completo |
inSampleSize |
Solicita una decodificación con dimensiones reducidas |
| EXIF | Metadatos que pueden acompañar una fotografía |
| Multipart | Tipo de cuerpo HTTP dividido en partes con nombre |
| Almacén de objetos | Servicio que guarda y recupera objetos mediante identificadores |
SavedStateHandle |
Estado pequeño asociado a un ViewModel que puede restaurarse |
rememberSaveable |
Estado de Compose que participa en los mecanismos de estado guardado |
13. Preguntas de autoevaluación
Comprensión conceptual
- ¿Por qué una app suele utilizar Coil para presentar imágenes de red en lugar de hacer la descarga dentro de cada
Composable? - ¿Qué problema aparece si Coil usa un cliente HTTP distinto del que tiene el interceptor de autenticación?
- ¿Por qué una imagen almacenada en caché puede ocultar un error de red?
- ¿Qué diferencia existe entre una URI
content://, una ruta de archivo y una clave de imagen remota? - ¿Por qué Photo Picker permite seleccionar una fotografía sin conceder acceso general a la galería?
- ¿Qué función cumple
FileProvidercuando la captura se realiza desde otra aplicación de cámara? - ¿Por qué el archivo JPEG y el bitmap decodificado tienen tamaños de memoria muy diferentes?
- ¿Qué dos riesgos distintos pueden surgir del EXIF y cómo se atienden?
Interpretación de código
- En
AsyncImage, ¿qué aportanaspectRatio(16f / 9f)yContentScale.Crop? ¿Qué inconveniente puede tener el segundo? - ¿Qué efecto tiene
inJustDecodeBounds = trueal decodificar una fotografía? - Una imagen de 4000 × 3000 se decodifica con
inSampleSize = 4. ¿Cuánto ocuparán aproximadamente sus píxeles si se usan cuatro bytes por píxel? - ¿Qué indican
@Multiparty@Parten una función de Retrofit? - ¿Por qué importa el valor
"archivo"enMultipartBody.Part.createFormData? - ¿Por qué
ImagenIlegibledebe aparecer antes deIOExceptionsi la primera hereda de la segunda? - ¿Por qué puede ser necesario conservar la URI pendiente de la cámara con
rememberSaveable? - ¿Por qué no debe almacenarse un bitmap completo en
SavedStateHandle?
Resolución de situaciones
- Una foto se ve derecha en la vista previa, pero al subirla aparece acostada. ¿Dónde investigarías primero?
- La imagen se subió correctamente, pero falló la creación del registro que debía contenerla. ¿Qué problema puede quedar en el servidor?
- Después de que Android terminó la aplicación mientras se usaba la cámara, volvió el título, pero ya no se puede abrir la URI de la galería. ¿Cómo explicas esta diferencia?
- Un estudiante propone enviar siempre la imagen original, conservar todos sus metadatos y reducirla solo en el servidor. ¿Qué costos y riesgos habría que discutir?
14. Respuestas posibles
- Coil centraliza la descarga, la decodificación, la caché y la integración con Compose. Evita repetir infraestructura y administrar manualmente solicitudes al recomponer o desplazar una lista.
- Las imágenes protegidas pueden solicitarse sin
Authorization; el servidor puede devolver 401. También se pierden interceptores y configuración de renovación de tokens si no se comparten o configuran explícitamente. - La biblioteca puede mostrar una copia descargada previamente sin pedir el recurso otra vez. La pantalla parece correcta aunque las nuevas peticiones fallen.
- La URI identifica contenido administrado por un proveedor y puede exigir permisos; una ruta se refiere a un archivo en un sistema de archivos; una clave remota identifica un objeto del servidor.
- Porque el usuario elige archivos concretos y el sistema concede acceso limitado a lo seleccionado. La app no necesita recorrer toda la biblioteca de imágenes.
- Traduce el acceso a un archivo privado a una URI compartible y permite que la cámara escriba en ese archivo con permisos delimitados.
- JPEG es un formato comprimido; el bitmap almacena píxeles decodificados. Aproximadamente, cada píxel puede ocupar cuatro bytes, independientemente del tamaño del archivo JPEG.
- EXIF puede determinar cómo debe orientarse la imagen y puede revelar datos privados, como coordenadas. Se interpreta la orientación y se crea una salida que no arrastre metadatos no necesarios.
aspectRatioreserva un rectángulo estable.Croplo llena, pero puede recortar contenido significativo de la imagen.- Permite conocer dimensiones sin construir el bitmap completo, para elegir previamente una escala de decodificación.
- Aproximadamente 1000 × 750 × 4 = 3 MB, sin contar otras estructuras o copias temporales.
- Declaran una solicitud HTTP multipart y la parte que contiene el archivo. Sin
@Multipart, Retrofit no interpreta adecuadamente un parámetro@Part. - El servidor identifica cada parte por su nombre. Si espera
archivoy recibe otro, puede rechazar el cuerpo por no cumplir el contrato. - Porque un
catch (IOException)anterior atraparía tambiénImagenIlegible, impidiendo mostrar un mensaje específico sobre el archivo ilegible. - Al abrir la cámara, Android puede destruir el proceso de la aplicación que la llamó. Si se restaura el estado de la actividad, la URI conservada permite identificar dónde se guardó la foto.
- Porque el estado guardado debe ser pequeño; un bitmap puede ocupar megabytes y exceder los límites de transferencia del estado. Se guarda una referencia accesible, no todos los píxeles.
- Primero comprobaría la lectura y aplicación de la orientación EXIF durante la preparación. El visor puede respetar EXIF aunque el compresor no lo haga.
- Puede quedar una imagen sin referencia, llamada objeto huérfano. Se requiere una política de limpieza, expiración o reutilización en reintentos.
- El título se restauró desde estado guardado, pero una URI no garantiza que el permiso de lectura siga vigente. Se necesita un mecanismo apropiado para conservar acceso o una copia privada.
- Aumenta la transferencia de datos, la latencia y el consumo de memoria; puede revelar ubicación y otros metadatos. Reducir en el servidor puede ser útil en algunos sistemas, pero no elimina los costos de subir el original.
15. Cierre y preguntas de reflexión
La gestión de imágenes combina interfaz, almacenamiento, comunicación de red y seguridad. El resultado visible suele ser una fotografía dentro de una tarjeta, pero su implementación depende de decisiones que deben mantenerse correctas incluso cuando falla la red, cambia el ciclo de vida o el sistema termina el proceso.
- ¿Cómo cambia tu estrategia cuando la imagen es pública frente a cuando solo la puede consultar una persona autenticada?
- ¿Qué debería ocurrir si el usuario abandona la pantalla después de capturar una fotografía pero antes de publicarla?
- ¿Qué datos y referencias deben sobrevivir a la recreación del proceso y cuáles deben reconstruirse?
- ¿Cómo comprobarías que una compresión conserva detalle suficiente y elimina los metadatos que no deben publicarse?
- ¿Qué solución diseñarías para evitar imágenes huérfanas o publicaciones duplicadas después de un fallo parcial?