Contenido del curso

Servicios de Localización

CameraViewModel con Hilt y StateFlow

Resumen

Cuando trabajas con la cámara en Android, necesitas una arquitectura clara que separe las acciones del usuario de los estados de la interfaz. Aquí aprenderás a definir los intents, el estado de UI y el ViewModel que controlan la lógica de una pantalla de cámara con Kotlin, Hilt y StateFlow.

Cómo defines los intents de la cámara con sealed interface

Los intents representan cada acción que el usuario puede ejecutar sobre la cámara. Al modelarlos con una sealed interface, garantizas que el when del ViewModel contemple todos los casos posibles.

Dentro de la carpeta presentation/camera creas el archivo CameraIntent y declaras la sealed interface. A partir de ahí, defines cada acción como una data class o un data object, según necesite o no parámetros.

Qué acciones necesita una pantalla de cámara

Estas son las cuatro acciones que estructuran el flujo:

  • PhotoTaken: una data class que recibe la foto como ByteArray. Representa el momento en que se captura una imagen.
  • SubmitCameraPermissionInfo: una data class con acceptedCameraPermission: Boolean y shouldShowCameraRationale: Boolean para gestionar el permiso del sensor.
  • PhotoSaved: un data object que indica que el usuario aceptó la foto.
  • CancelPreview: un data object que descarta la foto en preview.

¿Por qué una sealed interface y no un enum? Porque cada acción puede llevar datos distintos. La sealed interface permite que PhotoTaken cargue un ByteArray mientras que PhotoSaved no necesite parámetros. [02:10]

Por qué Kotlin pide equals y hashCode en un ByteArray

Cuando una data class contiene un ByteArray, el IDE sugiere generar equals y hashCode manualmente. La razón es técnica: un ByteArray compara referencias por defecto, no contenido.

Al dejar que Kotlin genere estas funciones según el contenido, obtienes un hash code que actúa como un serial único de la imagen. Si dos fotos tienen el mismo hash, tienen exactamente la misma información. Esto evita comparaciones erróneas y recomposiciones innecesarias en Compose. [01:30]

Cómo modelas el estado de UI de la cámara

El estado se concentra en una sola data class llamada CameraUIState. Tener una única fuente de verdad simplifica el render y evita estados inconsistentes entre el preview y la captura.

Los campos que componen el estado son:

  • isInPreviewMode: Boolean = false: arranca en false porque la pantalla inicia tomando una foto, no mostrándola.
  • lastSavedPhoto: File? = null: guarda el archivo que la cámara entrega cuando se confirma la foto.
  • showCameraRationale: Boolean = false: controla si debes mostrar la justificación del permiso.
  • permissionGranted: Boolean = false: indica si el usuario ya concedió acceso al sensor.

¿Qué es el preview mode en una cámara? Es el estado en el que ya tomaste una foto y la estás mostrando al usuario para que decida si la guarda o la descarta. [04:05]

Cómo construyes el CameraViewModel con Hilt y StateFlow

El CameraViewModel se anota con @HiltViewModel y extiende de ViewModel. Hilt se encarga de inyectar las dependencias en el constructor, lo que mantiene la clase desacoplada y testeable.

Qué dependencias inyectas en el constructor

El @Inject constructor recibe dos colaboradores:

  • photoHandler: gestiona la foto actual en preview y expone un flow con el ByteArray de la imagen.
  • locationTracker: asocia cada foto con la última ubicación conocida en el momento de la captura.

Esta combinación te permite enriquecer cada imagen con metadatos de geolocalización sin acoplar la lógica a la UI.

Cómo expones uiState y previewPhoto como flujos calientes

El uiState se declara como MutableStateFlow<CameraUIState > privado y se expone con asStateFlow() para que la UI no pueda mutarlo directamente.

El previewPhoto es más interesante: toma el flujo del photoHandler.getCurrentPreviewPhoto() y lo transforma con stateIn dentro del viewModelScope. La configuración usa SharingStarted.WhileSubscribed(5000), lo que significa que el flujo se mantiene activo cinco segundos después de que el último suscriptor se desconecta.

¿Para qué sirve stateIn con WhileSubscribed? Convierte un cold flow en un hot flow que retiene el último valor. Si el usuario rota la pantalla, no pierdes la foto en preview porque el flujo no se reinicia inmediatamente. [06:20]

Cómo manejas los intents dentro del ViewModel

La función pública del ViewModel recibe un CameraIntent y lo procesa con un when. Esta estructura te obliga a contemplar cada acción declarada en la sealed interface.

Por ahora solo se implementa el caso de permisos como plantilla:

kotlin when (cameraIntent) { is CameraIntent.SubmitCameraPermissionInfo -> { uiState.value = uiState.value.copy( showCameraRationale = cameraIntent.shouldShowCameraRationale, permissionGranted = cameraIntent.acceptedCameraPermission ) } }

Usar copy sobre el estado actual asegura que solo modificas los campos relevantes y mantienes la inmutabilidad de la data class. Los demás intents quedan listos como ramas vacías para implementarse cuando la UI empiece a emitir capturas y confirmaciones.

Con esta base ya tienes la columna vertebral de la pantalla: intents tipados, un estado único y un ViewModel preparado para recibir acciones desde Compose. ¿Cómo conectarías tu UI con estos intents? Cuéntame en los comentarios qué parte te gustaría profundizar.