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

Lectura
20 min~10 min lectura
Objetivo de la lección

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.

Puntos de control
  • Integrando la Cámara y la Galería en tu Aplicación Móvil
  • Concepto Clave: Permisos, URI y Manipulación de Medios
  • Cómo Funciona en la Práctica: El Flujo Paso a Paso
  • Código en Acción: Implementación Completa y Funcional

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}
        <Text style={styles.buttonText}>📁 Elegir de la Galería</Text>
      </TouchableOpacity>

      <TouchableOpacity style={styles.button}
        <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}
            <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.
Falar no WhatsApp
Laboratorio de práctica

Antes de marcar esta lección como completa, escribí una evidencia breve para Desarrollo de Apps Nativas con React Native y Expo: De Cero a Producción: un ejemplo, una decisión, una captura, una mini demo o una nota que puedas reutilizar en portfolio.

Reflexión rápida

¿Qué cambiarías en tu forma de trabajar después de aplicar uso de apis de dispositivo: cámara y galería?

De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión

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