Contenido del curso

Servicios de Localización

Cómo guardar fotos en Android con PhotoHandler

Resumen

Manejar la cámara en Android implica resolver dos retos clave: gestionar permisos y guardar fotos en el sistema local. Aquí aprenderás a construir un PhotoHandler en Kotlin que centraliza el flujo de captura, previsualización y almacenamiento de imágenes usando CameraX, Flow y coroutines.

Qué hace un PhotoHandler en una app Android

Un PhotoHandler funciona como contrato entre la cámara y el almacenamiento. Define qué operaciones expone tu feature de cámara y cómo se comunican entre sí.

En la capa de domain creas un paquete camera y dentro un archivo PhotoHandler con la interfaz de operaciones. Las funciones que necesitas son:

  • savePicturePreview(): suspend function que retorna un File? con la foto confirmada.
  • onCancelPreview(): suspend function que descarta la foto en preview.
  • getPhotos(): expone un Flow<List<File>> con las fotos guardadas.
  • getCurrentPreviewPhoto(): retorna un Flow<ByteArray?> con la foto temporal.
  • clearPhotos(): suspend function que libera recursos y borra archivos.
  • onPhotoPreview(photoBytes: ByteArray): envía la foto recién capturada al preview.

¿Qué es un PhotoHandler? Es una interfaz que define cómo capturar, previsualizar, guardar y eliminar fotos en una app Android, separando la lógica de cámara del almacenamiento local.

Cómo fluye una foto desde CameraX hasta el archivo guardado

El recorrido tiene una lógica clara. Cuando CameraX captura una imagen, llamas a onPhotoPreview con el byte array, que es básicamente un bitmap serializado. Ese byte array se expone vía getCurrentPreviewPhoto para que la UI lo muestre.

Si al usuario le gusta, ejecutas savePicturePreview y la foto se convierte en un File real en disco. Si no, onCancelPreview limpia el estado. Al final del recorrido, clearPhotos libera la memoria.

Cómo implementar PhotoHandlerImpl en la capa de data

En el módulo de data creas otro paquete camera con la clase PhotoHandlerImpl. Esta clase inyecta el Application Context como private val context: Context, porque necesitas acceder al sistema de archivos de la app.

Defines un photoDirectory inicializado con lazy, lo que significa que se crea solo cuando se usa por primera vez. La ruta apunta al directorio principal de archivos de la app concatenado con /fotos. Si la carpeta no existe, ejecutas mkdir() para crearla.

Luego declaras dos variables locales con MutableStateFlow:

  • currentPreviewPhoto: un MutableStateFlow<ByteArray?> que empieza en null.
  • photos: un MutableStateFlow<List<File>> que empieza con emptyList().

Cómo cargar fotos guardadas con loadSavedPhotos

La función privada loadSavedPhotos revisa si photoDirectory existe y trae todos los archivos con listFiles(). Aplica un filtro doble: que sea archivo y que tenga extensión válida.

Las extensiones aceptadas son jpg, jpeg y png, comparadas en lowercase. Después ordena los resultados de forma descendente por la última fecha de modificación. Si el directorio no existe, devuelve emptyList().

Esta función se ejecuta en el bloque init de la clase, así que apenas se invoca el PhotoHandlerImpl, ya tienes la lista de fotos cargada.

Por qué usar Dispatchers.IO para guardar y borrar fotos

Las operaciones de archivo bloquean el hilo principal y degradan la experiencia. Por eso, tanto clearPhotos como savePicturePreview usan withContext(Dispatchers.IO) para mover el trabajo a un hilo optimizado para entrada y salida.

En clearPhotos, recorres los archivos, llamas a delete() y luego loadSavedPhotos() refresca el Flow expuesto. En savePicturePreview, primero validas que exista una foto actual. Si la hay, generas un nombre con timestamp usando Locale y Date, creas el File dentro de photoDirectory y escribes el byte array con FileOutputStream.

¿Por qué se usa Dispatchers.IO? Porque las operaciones de lectura y escritura en disco son bloqueantes. Ejecutarlas en el hilo principal congela la UI; Dispatchers.IO las mueve a un pool de hilos diseñado para esto.

Qué pasa después de guardar una foto

Una vez escrito el archivo, llamas de nuevo a loadSavedPhotos() para que el Flow refleje la foto recién agregada. Después limpias currentPreviewPhoto.value = null para cerrar el ciclo de preview.

La función retorna el File guardado, cumpliendo el contrato de la interfaz. Si ocurre una excepción durante el proceso, devuelves null para que la capa superior maneje el error sin romper la app.

Cómo se exponen los Flows al resto de la app

La exposición de datos usa asStateFlow(), que convierte un MutableStateFlow en su versión inmutable. Esto es importante para que las capas externas solo puedan observar, nunca modificar el estado.

  • getPhotos() retorna photos.asStateFlow().
  • getCurrentPreviewPhoto() retorna currentPreviewPhoto.asStateFlow().
  • onPhotoPreview simplemente asigna currentPreviewPhoto.value = photoBytes.
  • onCancelPreview asigna currentPreviewPhoto.value = null.

Con esto, tu PhotoHandler queda listo para inyectarse en el árbol de dependencias. El siguiente paso es entender la traducción entre bitmaps y byte arrays, que es donde la cámara y el almacenamiento se encuentran de verdad.

¿Has implementado un sistema de cámara similar en tus proyectos? Cuéntame en los comentarios qué retos enfrentaste con permisos o almacenamiento.