Saltar al contenido

Lección 11 de 20 · Lectura y práctica

Uso de APIs de Dispositivo: Cámara y Galería

Integrando la Cámara y la Galería en tu Aplicación Móvil En el mundo del desarrollo móvil moderno, la capacidad de interactuar con el hardware del dispositivo es lo que separa una aplicación básica de una experiencia rica y.

Del concepto a la práctica

Taller de esta lección

Cámara y galería deben modelarse como un flujo de permiso, selección y resultado. Una cancelación no es un error técnico: es una decisión del usuario. La aplicación debe conservar la pantalla utilizable en ambos casos y no asumir que siempre llega una URI.

Ejemplo resuelto

El estado inicial es idle; tras pedir permiso puede ser denied o granted. Si la galería devuelve una selección cancelada, el estado de selección conserva su valor anterior; en esta prueba inicialmente permanece idle. Si devuelve file://foto-7.jpg, se muestra una vista previa y se conserva esa referencia para preview, sin confundirla con copiar el archivo.

Pruébalo

  1. Solicita el permiso antes de abrir la capacidad.

  2. Representa permiso rechazado con una acción para reintentar.

  3. Comprueba explícitamente si el usuario canceló.

  4. Muestra una vista previa solo cuando existe un recurso válido.

Error frecuente: tratar una cancelación como si fuera una URI vacía y pasarla a la imagen. Corrección: valida el resultado antes de renderizar o persistir.

Comprueba tu resultado

Ejercicio: ejecuta tres casos: permiso rechazado, selección cancelada y selección exitosa con file://foto-7.jpg. Resultado esperado: los dos primeros muestran una respuesta estable sin vista previa; el tercero muestra exactamente la imagen elegida y su estado de selección.

Selección con permisos independientes

Para galería usa la API compatible de ImagePicker y mediaTypes:['images'] en SDK actuales, en vez del enum histórico deprecado. Cámara y galería no deben bloquearse mutuamente porque una carezca de permiso; solicita cada acceso cuando el usuario lo elija. Ten en cuenta permisos por plataforma/SDK y el selector del sistema. No fuerces reintentos si canAskAgain=false.

El fixture file://foto-7.jpg es un identificador ficticio para una prueba simulada de estados; no existe una imagen real en esa ruta. En dispositivo, preview requiere el URI devuelto por result.assets[0].uri. Comprueba canceled primero, después assets y uri; cancelación conserva la selección anterior si la había, en lugar de borrarla accidentalmente. Si la pantalla empieza sin selección, permanece sin preview. Capture usa CameraView para vista en vivo o launchCameraAsync para la UI del sistema; son flujos distintos.

Capturas suelen ir a caché y almacenar la URI no copia el archivo. FileSystem.getInfoAsync desde el import principal histórico ya no es la API recomendada: usa File/Paths, o importa la API histórica explícitamente desde expo-file-system/legacy. No registres EXIF ni ubicación completa sin necesidad. ImagePicker.

Solicitud de permiso lleva a acceso autorizado o denegado, con alternativa y acceso a ajustes del sistema.
CursaloMapa de práctica
Pide permiso cuando hace falta

Solicita el permiso cuando la tarea lo necesita y explica su propósito. Si se deniega, ofrece una alternativa y respeta la decisión; si el sistema ya no permite volver a solicitarlo, orienta hacia ajustes sin prometer acceso.

Ver la infografía en tamaño completo ↗

Integrando la Cámara y la Galería en tu Aplicación Móvil

En el mundo del desarrollo móvil moderno, la capacidad de interactuar con el hardware del dispositivo es lo que separa una aplicación básica de una experiencia rica y envolvente. Entre todas las APIs nativas, el acceso a la cámara y la galería de fotos es uno de los más solicitados por los usuarios y, por tanto, uno de los más críticos para dominar. Esta funcionalidad permite a tus aplicaciones ir más allá de la pantalla, capturando momentos del mundo real, subiendo avatares, escaneando documentos o compartiendo contenido visual. En React Native con Expo, este proceso, que históricamente era complejo y propenso a errores de configuración nativa, se ha simplificado enormemente gracias a módulos bien diseñados y documentados.

Esta lección está diseñada para llevarte desde la teoría hasta la implementación práctica y robusta. No solo aprenderás a abrir la cámara y seleccionar una imagen, sino que profundizaremos en la gestión de permisos, el manejo de diferentes formatos y calidades, la optimización del rendimiento y las mejores prácticas para una experiencia de usuario fluida. Comprenderás el flujo completo, desde que el usuario toca un botón hasta que tu aplicación tiene la imagen lista para ser procesada, subida a un servidor o mostrada en pantalla. Abordaremos tanto el uso de la cámara en tiempo real como la selección de medios de la biblioteca del dispositivo, cubriendo los escenarios más comunes que encontrarás en proyectos reales.

Concepto Clave: Permisos, URI y Manipulación de Medios

Antes de escribir una sola línea de código, es fundamental entender tres pilares conceptuales. Primero, los permisos. En iOS y Android, el acceso a la cámara y al almacenamiento está estrictamente controlado por el sistema operativo por razones de privacidad y seguridad. Tu aplicación debe solicitar autorización al usuario de manera explícita y clara, y estar preparada para manejar tanto la aceptación como la denegación de forma elegante. Piensa en esto como pedir la llave a un guardia de seguridad antes de entrar a una sala especial; sin su aprobación, simplemente no puedes proceder.

Segundo, el URI (Uniform Resource Identifier). Cuando capturas o seleccionas una imagen, el sistema no te devuelve el archivo de imagen en sí dentro de tu código JavaScript. En su lugar, te proporciona un URI, que es esencialmente una ruta o dirección única que apunta a la ubicación de ese archivo en el dispositivo. Es como recibir un número de estantería en un almacén gigante. Usando este "número", puedes cargar la imagen, mostrarla, o copiarla a un lugar más permanente dentro del espacio de almacenamiento de tu app. Trabajar con URIs es más eficiente en memoria que manejar archivos binarios directamente en el entorno de JavaScript.

Tercero, la manipulación de medios. Las imágenes capturadas por dispositivos modernos son enormes, a menudo de varios megabytes. Subir o procesar una imagen a su resolución original puede ser lento y costoso en datos. Por lo tanto, casi siempre necesitarás manipularla: redimensionarla, comprimirla, cambiar su formato o recortarla. Esta es una etapa crucial para el rendimiento de tu aplicación y la satisfacción del usuario. Es el equivalente a editar una fotografía antes de enviarla por correo: ajustas el tamaño y la calidad para que sea adecuada para su propósito final.

Cómo Funciona en la Práctica: El Flujo Paso a Paso

El proceso para integrar la cámara o la galería sigue un patrón bien definido. Primero, debes instalar y configurar los módulos necesarios de Expo. Para la cámara, usarás principalmente expo-camera, y para la galería, expo-image-picker. Expo maneja la configuración nativa por ti, pero es vital asegurarse de que tu proyecto está en el SDK correcto y de ejecutar los comandos de prebuild si estás en un flujo de desarrollo bare workflow.

El segundo paso, y el más crítico, es la solicitud de permisos. Al iniciar tu aplicación o justo antes de usar la funcionalidad, debes verificar el estado de los permisos. Si no se han concedido, debes solicitar al usuario que los otorgue. Tu código debe manejar todos los estados posibles: "granted" (otorgado), "denied" (denegado) y "undetermined" (no decidido). Si el usuario deniega el permiso, una buena práctica es guiarlo amablemente hacia la configuración de la aplicación para que pueda cambiarlo manualmente, usando funciones como `Linking.openSettings()`.

Una vez obtenidos los permisos, el tercer paso es lanzar la interfaz nativa` de `expo-camera` para una vista previa en tiempo real o `ImagePicker.launchCameraAsync()` para una experiencia más simple. Estas funciones abren la interfaz de usuario nativa del sistema (la app de cámara o el selector de fotos), lo que garantiza una experiencia familiar para el usuario. Finalmente, cuando el usuario toma o selecciona una imagen, la promesa se resuelve devolviendo un objeto con la propiedad `uri` y otra información (como `width`, `height`, `type`). Con ese URI en mano, puedes proceder al cuarto paso: procesar y utilizar la imagen, mostrándola en un componente `` o subiéndola a tu backend.

Código en Acción: Implementación Completa y Funcional

A continuación, veremos un ejemplo práctico y completo que integra tanto la selección desde la galería como la captura desde la cámara, con gestión de permisos y visualización del resultado. Este componente puede ser integrado directamente en tu aplicación.


import React, { useState, useEffect } from 'react';
import { View, Text, Image, StyleSheet, Alert, ScrollView } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import * as ImagePicker from 'expo-image-picker';
import * as MediaLibrary from 'expo-media-library';
import * as FileSystem from 'expo-file-system';

const CameraGalleryExample = () => {
  const [selectedImage, setSelectedImage] = useState(null);
  const [permissionsGranted, setPermissionsGranted] = useState(false);

  // 1. SOLICITAR PERMISOS AL MONTAR EL COMPONENTE
  useEffect(() => {
    (async () => {
      // Solicitar permisos para la galería (LIBRARY) y la cámara (CAMERA)
      const { status: galleryStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync();
      const { status: cameraStatus } = await ImagePicker.requestCameraPermissionsAsync();
      
      if (galleryStatus !== 'granted' || cameraStatus !== 'granted') {
        Alert.alert(
          'Permisos necesarios',
          'Lo siento, necesitamos permisos de cámara y galería para que esto funcione.',
          [{ text: 'OK', onPress: () => console.log('Permiso denegado') }]
        );
        setPermissionsGranted(false);
      } else {
        setPermissionsGranted(true);
      }
    })();
  }, []);

  // 2. FUNCIÓN PARA ELEGIR IMAGEN DE LA GALERÍA
  const pickImageFromGallery = async () => {
    if (!permissionsGranted) {
      Alert.alert('Permisos no concedidos');
      return;
    }

    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images, // Solo imágenes
      allowsEditing: true, // Permite edición (recorte)
      aspect: [4, 3], // Relación de aspecto para el recorte
      quality: 0.7, // Calidad comprimida (70%)
      base64: false, // No incluir base64 por defecto (usa URI)
    });

    console.log('Resultado del selector:', result);

    if (!result.canceled && result.assets && result.assets.length > 0) {
      const firstAsset = result.assets[0];
      setSelectedImage(firstAsset.uri);
      // Opcional: Guardar una copia en el álbum de la app
      await saveImageToAppAlbum(firstAsset.uri);
    }
  };

  // 3. FUNCIÓN PARA TOMAR FOTO CON LA CÁMARA
  const takePhotoWithCamera = async () => {
    if (!permissionsGranted) {
      Alert.alert('Permisos no concedidos');
      return;
    }

    let result = await ImagePicker.launchCameraAsync({
      allowsEditing: true,
      aspect: [16, 9], // Formato panorámico para la cámara
      quality: 0.8,
      exif: true, // Incluir datos EXIF (ubicación, etc.)
    });

    console.log('Resultado de la cámara:', result);

    if (!result.canceled && result.assets && result.assets.length > 0) {
      const firstAsset = result.assets[0];
      setSelectedImage(firstAsset.uri);
    }
  };

  // 4. FUNCIÓN AUXILIAR: GUARDAR IMAGEN EN UN ÁLBUM
  const saveImageToAppAlbum = async (fileUri) => {
    try {
      const { status } = await MediaLibrary.requestPermissionsAsync();
      if (status === 'granted') {
        const asset = await MediaLibrary.createAssetAsync(fileUri);
        const album = await MediaLibrary.getAlbumAsync('MiAppExpo');
        if (album === null) {
          await MediaLibrary.createAlbumAsync('MiAppExpo', asset, false);
        } else {
          await MediaLibrary.addAssetsToAlbumAsync([asset], album, false);
        }
        Alert.alert('Éxito', 'Imagen guardada en el álbum "MiAppExpo".');
      }
    } catch (error) {
      console.error('Error guardando en galería:', error);
    }
  };

  // 5. FUNCIÓN PARA OBTENER INFORMACIÓN DEL ARCHIVO
  const getImageInfo = async () => {
    if (selectedImage) {
      const fileInfo = await FileSystem.getInfoAsync(selectedImage);
      Alert.alert('Info del Archivo', `Tamaño: ${(fileInfo.size / 1024 / 1024).toFixed(2)} MB\nURI: ${selectedImage}`);
    }
  };

  return (
    <ScrollView contentContainerStyle={styles.container}>
      <Text style={styles.title}>Ejemplo Cámara y Galería</Text>
      
      <TouchableOpacity style={styles.button} onPress={pickImageFromGallery}>
        <Text style={styles.buttonText}>📁 Elegir de la Galería</Text>
      </TouchableOpacity>

      <TouchableOpacity style={styles.button} onPress={takePhotoWithCamera}>
        <Text style={styles.buttonText}>📸 Tomar una Foto</Text>
      </TouchableOpacity>

      {selectedImage && (
        <View style={styles.imageContainer}>
          <Image source={{ uri: selectedImage }} style={styles.image} />
          <TouchableOpacity style={styles.infoButton} onPress={getImageInfo}>
            <Text style={styles.infoButtonText}>Ver Información de la Imagen</Text>
          </TouchableOpacity>
          <Text style={styles.uriText} numberOfLines={2}>URI: {selectedImage}</Text>
        </View>
      )}

      {!permissionsGranted && (
        <Text style={styles.warning}>⚠️ Los permisos no están concedidos. La funcionalidad estará limitada.</Text>
      )}
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flexGrow: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 30,
    color: '#333',
  },
  button: {
    backgroundColor: '#007AFF',
    paddingVertical: 15,
    paddingHorizontal: 30,
    borderRadius: 10,
    marginBottom: 15,
    width: '100%',
    alignItems: 'center',
  },
  buttonText: {
    color: 'white',
    fontSize: 18,
    fontWeight: '600',
  },
  imageContainer: {
    marginTop: 30,
    alignItems: 'center',
    width: '100%',
  },
  image: {
    width: 300,
    height: 300,
    borderRadius: 10,
    borderWidth: 2,
    borderColor: '#ccc',
  },
  infoButton: {
    marginTop: 15,
    backgroundColor: '#34C759',
    padding: 10,
    borderRadius: 5,
  },
  infoButtonText: {
    color: 'white',
    fontWeight: '600',
  },
  uriText: {
    marginTop: 10,
    fontSize: 12,
    color: '#666',
    textAlign: 'center',
    paddingHorizontal: 10,
  },
  warning: {
    marginTop: 20,
    color: '#FF3B30',
    fontWeight: 'bold',
    textAlign: 'center',
  },
});

export default CameraGalleryExample;

Este componente funcional demuestra el flujo completo: solicitud de permisos combinada, dos botones para las diferentes acciones, visualización de la imagen seleccionada y una función auxiliar para obtener metadatos del archivo. Nota el uso de `allowsEditing: true`, que activa la interfaz de recorte nativa, y los parámetros `quality` y `aspect` para controlar el resultado. La función `saveImageToAppAlbum` muestra cómo, con permisos adicionales, puedes guardar las imágenes capturadas en un álbum específico de la galería del usuario, lo que mejora la organización.

Tip de Experiencia de Usuario: Siempre proporciona retroalimentación visual inmediata después de que el usuario seleccione o capture una imagen. Mostrar una vista previa, como hacemos con el componente `Image`, le confirma que la acción fue exitosa. Además, considera agregar un indicador de carga (`ActivityIndicator`) durante el procesamiento de imágenes grandes o la subida a un servidor, para evitar que el usuario piense que la aplicación se ha congelado.

Errores Comunes y Cómo Evitarlos

Uno de los errores más frecuentes es no verificar los permisos antes de intentar usar la API

El segundo error es malinterpretar la respuesta de la API

Un tercer error, con implicaciones de rendimiento y usabilidad, es no comprimir o redimensionar las imágenes

Cuarto, olvidar manejar los diferentes formatos y orientaciones de los dispositivos` de React Native o librerías como `expo-image` que manejan esto automáticamente, o procesa la rotación con `expo-image-manipulator`.

Finalmente, no probar en un dispositivo físico real

Checklist de Dominio

Para asegurarte de que has comprendido y puedes implementar correctamente el uso de las APIs de Cámara y Galería, verifica que puedes realizar o explicar cada uno de los siguientes puntos:

  • Solicitar y verificar permisos para la cámara (`CAMERA`) y la biblioteca de medios (`MEDIA_LIBRARY`) de manera independiente, manejando los estados "granted", "denied" y "undetermined".
  • Implementar dos funciones separadas (o parametrizadas) que utilicen `ImagePicker.launchImageLibraryAsync` y `ImagePicker.launchCameraAsync` para seleccionar y capturar imágenes, respectivamente.
  • Configurar correctamente las opciones del selector: definir `mediaTypes`, activar `allowsEditing` con una relación de `aspect`, ajustar la `quality` de compresión y decidir si necesitas `base64`.
  • Procesar la respuesta del selector de imágenes, accediendo de forma segura a la propiedad `uri` dentro del array `assets` y manejando el caso en que el usuario cancele la operación (`canceled: true`).
  • Mostrar una vista previa de la imagen seleccionada utilizando el componente `` de React Native con la prop `source={{ uri: ... }}` y estilizarla apropiadamente.
  • Realizar una operación posterior con la imagen, como obtener sus metadatos (`FileSystem.getInfoAsync`), redimensionarla/comprimirla (`expo-image-manipulator`) o subirla a un servidor mediante una petición `FormData`.
  • Manejar errores potenciales de forma elegante: permisos denegados, cancelación del usuario, errores de lectura de archivos o fallos en la red durante la subida, mostrando alertas informativas al usuario.
  • Probar la funcionalidad completa en un dispositivo físico, verificando que el flujo de permisos, la interfaz nativa y el procesamiento posterior funcionan tanto en iOS como en Android.

Sigue con React Native y Expo: APIs Nativas y Comercio Móvil

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.