Manejo de multimedia, permisos y funcionalidades del dispositivo en React Native

Capítulo 9

Tiempo estimado de lectura: 11 minutos

+ Ejercicio

Qué implica “multimedia y funcionalidades del dispositivo”

En una app real, el usuario espera poder tomar una foto, elegirla desde la galería, adjuntar un PDF, compartir un archivo o guardar contenido. En React Native esto suele implicar: acceso a cámara/galería, lectura de archivos, y un manejo correcto de permisos (iOS/Android) con estados intermedios (limitado, denegado, “no volver a preguntar”). El objetivo no es “pedir permisos al inicio”, sino hacerlo de forma contextual: cuando el usuario intenta usar la función, con mensajes claros y alternativas si no concede acceso.

Decisiones clave antes de implementar

  • Qué librería usar: para cámara/galería suele funcionar bien react-native-image-picker; para documentos, react-native-document-picker. Si tu proyecto usa Expo, el enfoque equivalente sería con módulos de Expo, pero aquí nos centraremos en React Native CLI.
  • Qué necesitas realmente: para elegir una imagen de galería normalmente no necesitas permiso explícito en Android 13+ si usas el selector del sistema; para cámara sí. En iOS, siempre debes declarar el motivo de uso en Info.plist.
  • Cómo representar el estado: “sin archivo”, “cargando”, “seleccionado”, “error”, “permiso denegado”, “permiso bloqueado”. Esto evita UI inconsistente.

Permisos: pedirlos bien (contextual) y manejar denegaciones

Estados típicos que debes contemplar

  • Granted: puedes proceder.
  • Denied: el usuario rechazó; puedes volver a pedir en otro momento (según plataforma).
  • Blocked (o “no volver a preguntar”): no puedes volver a pedir; debes guiar al usuario a Ajustes.
  • Limited (iOS Fotos): acceso limitado a algunas fotos; debes permitir que el usuario elija y, si hace falta, ampliar desde Ajustes.

Recomendación práctica: encapsula permisos en una utilidad

Centraliza la lógica para que tu UI solo consuma un resultado claro. Una opción común es usar react-native-permissions para unificar iOS/Android.

// permissions.ts (ejemplo orientativo)import { Platform } from 'react-native';import { check, request, openSettings, PERMISSIONS, RESULTS } from 'react-native-permissions';export type PermissionOutcome =  | { ok: true }  | { ok: false; reason: 'denied' | 'blocked' | 'unavailable' | 'limited'; openSettings?: () => void };export async function ensureCameraPermission(): Promise<PermissionOutcome> {  const perm = Platform.select({    ios: PERMISSIONS.IOS.CAMERA,    android: PERMISSIONS.ANDROID.CAMERA,  });  if (!perm) return { ok: false, reason: 'unavailable' };  const current = await check(perm);  if (current === RESULTS.GRANTED) return { ok: true };  if (current === RESULTS.BLOCKED) {    return { ok: false, reason: 'blocked', openSettings };  }  const next = await request(perm);  if (next === RESULTS.GRANTED) return { ok: true };  if (next === RESULTS.BLOCKED) return { ok: false, reason: 'blocked', openSettings };  if (next === RESULTS.LIMITED) return { ok: false, reason: 'limited' };  return { ok: false, reason: 'denied' };}

Patrón de UX recomendado

  • Botón → explicación breve → pedir permiso: “Para tomar una foto necesitamos acceso a la cámara”.
  • Si deniega: muestra un mensaje con alternativa (p. ej., “Elegir de galería” o “Adjuntar archivo”).
  • Si está bloqueado: muestra CTA “Abrir Ajustes”.
  • No repitas prompts: si el usuario deniega, no vuelvas a pedir inmediatamente en bucle; espera una acción explícita.

Integración de cámara y galería (imagen)

Selección de imagen con react-native-image-picker

Esta librería puede abrir el selector del sistema (galería) o la cámara. El resultado típico incluye assets con uri, fileName, type, fileSize, width, height.

// media.ts (ejemplo orientativo)import { launchCamera, launchImageLibrary, ImageLibraryOptions, CameraOptions } from 'react-native-image-picker';export async function pickFromGallery() {  const options: ImageLibraryOptions = {    mediaType: 'photo',    selectionLimit: 1,    quality: 0.8, // compresión básica    includeBase64: false,  };  const res = await launchImageLibrary(options);  if (res.didCancel) return null;  if (res.errorCode) throw new Error(res.errorMessage || res.errorCode);  const asset = res.assets?.[0];  if (!asset?.uri) return null;  return asset;}export async function takePhoto() {  const options: CameraOptions = {    mediaType: 'photo',    cameraType: 'front',    quality: 0.8,    saveToPhotos: false,  };  const res = await launchCamera(options);  if (res.didCancel) return null;  if (res.errorCode) throw new Error(res.errorMessage || res.errorCode);  const asset = res.assets?.[0];  if (!asset?.uri) return null;  return asset;}

Fallos típicos y cómo prevenirlos

  • URI inválida o sin permisos: valida asset.uri antes de renderizar/subir.
  • Imagen enorme: limita calidad y, si tu caso lo requiere, redimensiona con una librería de manipulación de imágenes (si no, al menos controla tamaño máximo permitido).
  • Bloqueo por memoria: evita base64 para previews; usa uri y un placeholder.
  • iOS Info.plist: si faltan descripciones de uso, la app puede fallar al solicitar acceso. Asegúrate de declarar motivos para cámara/fotos.

Adjuntar archivos (PDF, DOCX, etc.)

Selección de documentos con react-native-document-picker

Para adjuntar archivos a un formulario, el selector del sistema suele ser la opción más robusta. Te devuelve metadatos como nombre, tipo MIME y tamaño.

// files.ts (ejemplo orientativo)import DocumentPicker from 'react-native-document-picker';export async function pickDocument() {  try {    const res = await DocumentPicker.pickSingle({      type: [DocumentPicker.types.pdf, DocumentPicker.types.images, DocumentPicker.types.plainText],    });    return res; // { uri, name, type, size }  } catch (e: any) {    if (DocumentPicker.isCancel(e)) return null;    throw e;  }}

Validaciones recomendadas

  • Tamaño máximo: p. ej. 5MB para imágenes, 10MB para PDF.
  • Tipo permitido: valida MIME (image/*, application/pdf).
  • Nombre: muestra el nombre y permite “quitar adjunto”.

Optimización de carga de imágenes: tamaños, placeholders y experiencia

Estrategia simple y efectiva

  • Preview local inmediata: renderiza la imagen con uri apenas se selecciona.
  • Placeholder mientras carga: muestra un contenedor con color neutro y un indicador de carga.
  • Evita re-render costoso: memoriza el componente de preview si depende de props grandes.
  • Limita dimensiones: para avatar, recorta/ajusta a un tamaño fijo (p. ej. 128x128) en UI; en backend, procesa a tamaños estándar.

Ejemplo de componente de avatar con placeholder

import React from 'react';import { View, Image, ActivityIndicator, StyleSheet } from 'react-native';export function AvatarPreview({ uri, loading }: { uri?: string; loading?: boolean }) {  return (    <View style={styles.wrap}>      {uri ? (        <Image          source={{ uri }}          style={styles.img}          resizeMode="cover"          onLoadStart={() => {}}          onError={() => {}}        />      ) : (        <View style={styles.placeholder} />      )}      {loading ? <View style={styles.overlay}><ActivityIndicator /></View> : null}    </View>  );}const styles = StyleSheet.create({  wrap: { width: 128, height: 128, borderRadius: 64, overflow: 'hidden' },  img: { width: '100%', height: '100%' },  placeholder: { flex: 1, backgroundColor: '#E5E7EB' },  overlay: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.15)' },});

Práctica guiada: foto de perfil o adjuntar archivo a un formulario

Implementarás un flujo completo con: selección (cámara/galería/archivo), validaciones, estados de UI, y feedback visual. El ejemplo asume que ya tienes un formulario funcionando y solo añadirás el campo “Adjunto” o “Avatar”.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

1) Define el modelo de adjunto y estados de UI

type SelectedImage = {  uri: string;  fileName?: string;  type?: string;  fileSize?: number;  width?: number;  height?: number;};type SelectedDoc = {  uri: string;  name: string;  type?: string;  size?: number;};type Attachment =  | { kind: 'image'; data: SelectedImage }  | { kind: 'doc'; data: SelectedDoc };type AttachState = {  attachment: Attachment | null;  busy: boolean;  error: string | null;};

2) Crea acciones: tomar foto, elegir de galería, elegir documento

La clave es: pedir permiso solo al usar cámara; galería/document picker suelen funcionar con selector del sistema, pero igual debes manejar errores.

import React, { useState } from 'react';import { View, Text, Pressable, Alert } from 'react-native';import { ensureCameraPermission } from './permissions';import { takePhoto, pickFromGallery } from './media';import { pickDocument } from './files';export function AttachmentField() {  const [state, setState] = useState<AttachState>({ attachment: null, busy: false, error: null });  const setBusy = (busy: boolean) => setState(s => ({ ...s, busy }));  const setError = (error: string | null) => setState(s => ({ ...s, error }));  async function onTakePhoto() {    setError(null);    const perm = await ensureCameraPermission();    if (!perm.ok) {      if (perm.reason === 'blocked') {        Alert.alert('Permiso bloqueado', 'Activa el acceso a la cámara en Ajustes.', [          { text: 'Cancelar', style: 'cancel' },          { text: 'Abrir Ajustes', onPress: () => perm.openSettings?.() },        ]);        return;      }      Alert.alert('Permiso requerido', 'No se puede abrir la cámara sin permiso.');      return;    }    try {      setBusy(true);      const img = await takePhoto();      if (!img) return;      validateImageOrThrow(img);      setState(s => ({ ...s, attachment: { kind: 'image', data: img }, error: null }));    } catch (e: any) {      setError(e.message || 'No se pudo tomar la foto');    } finally {      setBusy(false);    }  }  async function onPickGallery() {    setError(null);    try {      setBusy(true);      const img = await pickFromGallery();      if (!img) return;      validateImageOrThrow(img);      setState(s => ({ ...s, attachment: { kind: 'image', data: img }, error: null }));    } catch (e: any) {      setError(e.message || 'No se pudo seleccionar la imagen');    } finally {      setBusy(false);    }  }  async function onPickDoc() {    setError(null);    try {      setBusy(true);      const doc = await pickDocument();      if (!doc) return;      validateDocOrThrow(doc);      setState(s => ({ ...s, attachment: { kind: 'doc', data: doc }, error: null }));    } catch (e: any) {      setError(e.message || 'No se pudo seleccionar el archivo');    } finally {      setBusy(false);    }  }  function onClear() {    setState({ attachment: null, busy: false, error: null });  }  return (    <View style={{ gap: 12 }}>      <Text style={{ fontWeight: '600' }}>Adjunto</Text>      <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>        <Pressable onPress={onTakePhoto} disabled={state.busy}><Text>Tomar foto</Text></Pressable>        <Pressable onPress={onPickGallery} disabled={state.busy}><Text>Galería</Text></Pressable>        <Pressable onPress={onPickDoc} disabled={state.busy}><Text>Archivo</Text></Pressable>        <Pressable onPress={onClear} disabled={state.busy || !state.attachment}><Text>Quitar</Text></Pressable>      </View>      {state.error ? <Text style={{ color: 'crimson' }}>{state.error}</Text> : null}      <AttachmentPreview attachment={state.attachment} busy={state.busy} />    </View>  );}function validateImageOrThrow(img: SelectedImage) {  const maxBytes = 5 * 1024 * 1024;  if (img.fileSize != null && img.fileSize > maxBytes) {    throw new Error('La imagen supera 5MB');  }  if (img.type && !img.type.startsWith('image/')) {    throw new Error('Formato de imagen no permitido');  }}function validateDocOrThrow(doc: SelectedDoc) {  const maxBytes = 10 * 1024 * 1024;  if (doc.size != null && doc.size > maxBytes) {    throw new Error('El archivo supera 10MB');  }  if (doc.type && !['application/pdf', 'text/plain'].includes(doc.type) && !doc.type.startsWith('image/')) {    throw new Error('Tipo de archivo no permitido');  }}

3) Preview con feedback visual (imagen vs documento)

import React from 'react';import { View, Text, Image, ActivityIndicator } from 'react-native';export function AttachmentPreview({ attachment, busy }: { attachment: any; busy: boolean }) {  if (!attachment) {    return (      <View style={{ height: 120, borderRadius: 12, backgroundColor: '#F3F4F6', alignItems: 'center', justifyContent: 'center' }}>        <Text style={{ color: '#6B7280' }}>Sin adjunto</Text>      </View>    );  }  if (attachment.kind === 'image') {    return (      <View style={{ width: 128, height: 128, borderRadius: 64, overflow: 'hidden' }}>        <Image source={{ uri: attachment.data.uri }} style={{ width: '100%', height: '100%' }} />        {busy ? (          <View style={{ position: 'absolute', inset: 0, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.15)' }}>            <ActivityIndicator />          </View>        ) : null}      </View>    );  }  return (    <View style={{ padding: 12, borderRadius: 12, backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#E5E7EB' }}>      <Text style={{ fontWeight: '600' }} numberOfLines={1}>{attachment.data.name}</Text>      <Text style={{ color: '#6B7280' }}>{attachment.data.type || 'tipo desconocido'}</Text>      {busy ? <ActivityIndicator style={{ marginTop: 8 }} /> : null}    </View>  );}

4) Envío al backend: multipart/form-data (patrón común)

Para subir imagen o documento, normalmente usarás FormData. Asegúrate de enviar uri, type y name. En iOS, algunas URIs requieren normalización; si tu backend falla, revisa que el type sea correcto y que el name exista.

async function submitForm(values: any, attachment: Attachment | null) {  const fd = new FormData();  fd.append('displayName', values.displayName);  if (attachment?.kind === 'image') {    const img = attachment.data;    fd.append('avatar', {      uri: img.uri,      type: img.type || 'image/jpeg',      name: img.fileName || 'avatar.jpg',    } as any);  }  if (attachment?.kind === 'doc') {    const doc = attachment.data;    fd.append('file', {      uri: doc.uri,      type: doc.type || 'application/octet-stream',      name: doc.name || 'adjunto',    } as any);  }  const res = await fetch('https://tu-api.com/profile', {    method: 'POST',    body: fd,    headers: {      // No fuerces Content-Type; fetch lo setea con boundary en muchos casos      // 'Content-Type': 'multipart/form-data'    },  });  if (!res.ok) {    const text = await res.text();    throw new Error(text || 'Error al subir');  }  return res.json();}

5) Manejo de fallos típicos en el envío

ProblemaCausa comúnMitigación
El backend recibe archivo vacíouri incorrecta o falta name/typeValida uri, setea type y name por defecto
La app se queda “cargando”No se resetea estado en finallyUsa try/catch/finally y estados separados
Permiso bloqueadoUsuario marcó “no volver a preguntar” / AjustesDetecta blocked y ofrece Abrir Ajustes
Imagen demasiado pesadaFotos de cámara en alta resoluciónLimita quality, valida tamaño y rechaza con feedback
Crash o freeze al previsualizarUso de base64 o renders innecesariosEvita base64, usa uri y placeholder

Checklist de implementación (rápido)

  • Declarar descripciones de permisos en iOS (cámara/fotos) y configurar Android según necesidades.
  • Pedir permisos solo cuando el usuario toca “Tomar foto”.
  • Modelar estados: busy, error, attachment.
  • Validar tipo y tamaño antes de subir.
  • Preview inmediata con placeholder y opción “Quitar”.
  • Manejar blocked con CTA a Ajustes y alternativa (galería/archivo).

Ahora responde el ejercicio sobre el contenido:

Al implementar la acción “Tomar foto” en una app con React Native, ¿cuál es el enfoque recomendado para solicitar y manejar el permiso de cámara?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La recomendación es pedir permisos cuando el usuario intenta usar la cámara, manejar estados como denied y blocked, ofrecer alternativas (galería/archivo) y, si está bloqueado, mostrar una acción para abrir Ajustes sin insistir con solicitudes repetidas.

Siguiente capítulo

Rendimiento y optimización en React Native para producción

Arrow Right Icon
Portada de libro electrónico gratuitaReact Native desde Cero a App Profesional
75%

React Native desde Cero a App Profesional

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.