Integração com recursos do dispositivo em React Native: câmera e localização via bibliotecas

Capítulo 12

Tempo estimado de leitura: 12 minutos

+ Exercício

Objetivo e visão geral

Integração com recursos do dispositivo (como câmera e localização) envolve três pilares: (1) permissões e mensagens claras ao usuário, (2) fluxo de uso do recurso (capturar foto, obter coordenadas), e (3) tratamento de erros e fallbacks quando o recurso não existe, está desativado ou foi negado. Neste capítulo, vamos implementar câmera e localização usando bibliotecas consolidadas do ecossistema Expo: expo-camera, expo-media-library, expo-location e (opcional) expo-location para reverse geocoding.

Bibliotecas e por que escolhê-las

Expo Camera + Media Library

  • expo-camera: acesso à câmera com preview, captura de foto e controle de flash/zoom (quando suportado).
  • expo-media-library: salva a foto na galeria (Android/iOS) e gerencia permissões de mídia.

Expo Location

  • expo-location: solicita permissões, obtém posição atual com precisão e permite acompanhar atualizações em tempo real.
  • Reverse geocoding: converte coordenadas em endereço aproximado (rua/cidade) quando disponível.

Permissões: como pensar e como comunicar

Permissões não são apenas um popup: elas fazem parte da experiência. Boas práticas:

  • Peça permissão no momento certo (quando o usuário inicia a ação), não na abertura do app.
  • Explique por que precisa do recurso antes do prompt do sistema (mensagem curta e objetiva).
  • Trate explicitamente: aceite, recusa, recusa permanente (quando o usuário marca “não perguntar novamente” no Android) e recurso indisponível.
  • Ofereça fallback: permitir continuar sem câmera/localização, ou abrir configurações para habilitar.

iOS: descrições obrigatórias no Info.plist

No iOS, o sistema exige textos de justificativa. Em projetos Expo, configure no app.json / app.config.js:

{"expo":{"ios":{"infoPlist":{"NSCameraUsageDescription":"Precisamos da câmera para você tirar fotos e anexar ao seu cadastro.","NSLocationWhenInUseUsageDescription":"Usamos sua localização para sugerir conteúdo próximo e preencher seu endereço.","NSPhotoLibraryAddUsageDescription":"Precisamos salvar a foto na sua galeria quando você escolher."}}}}

Android: permissões e comportamento de recusa

No Android, além do prompt, pode existir “não perguntar novamente”. Quando isso ocorre, a única forma de reverter é via Configurações do sistema. Seu app deve detectar e orientar o usuário.

Instalação das dependências

Em um projeto Expo, instale:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

npx expo install expo-camera expo-media-library expo-location

Se você estiver em Bare React Native (sem Expo), o fluxo muda (ex.: react-native-vision-camera e bibliotecas de geolocalização). Aqui manteremos o fluxo Expo para reduzir fricção e focar em boas práticas.

Arquitetura de tela: estados essenciais

Para câmera e localização, é comum modelar estados explícitos:

  • permissionStatus: undetermined | granted | denied
  • isRequestingPermission: evita múltiplos prompts
  • error: mensagem amigável e log técnico separado
  • photoUri: URI da foto capturada para preview
  • isSaving: salvamento em andamento
  • location: coordenadas e metadados (accuracy, timestamp)
  • watchSubscription: referência para parar tracking

Implementação prática: captura de foto com preview e salvamento local

1) Componente de câmera com permissão e fallback

O fluxo recomendado:

  • Mostrar uma explicação e botão “Permitir câmera”.
  • Solicitar permissão ao clicar.
  • Se concedida, renderizar o preview da câmera.
  • Se negada, mostrar instruções e botão para abrir configurações.
import React, { useEffect, useRef, useState } from 'react';import { View, Text, Pressable, Image, ActivityIndicator, Platform, Linking } from 'react-native';import { CameraView, useCameraPermissions } from 'expo-camera';import * as MediaLibrary from 'expo-media-library';function PermissionBlock({ title, description, onRequest, onOpenSettings, isLoading }) {  return (    <View style={{ padding: 16, gap: 12 }}>      <Text style={{ fontSize: 18, fontWeight: '600' }}>{title}</Text>      <Text style={{ color: '#444' }}>{description}</Text>      <Pressable onPress={onRequest} style={{ backgroundColor: '#111', padding: 12, borderRadius: 8 }}>        {isLoading ? <ActivityIndicator color="#fff" /> : <Text style={{ color: '#fff', textAlign: 'center' }}>Permitir</Text>}      </Pressable>      <Pressable onPress={onOpenSettings} style={{ padding: 12 }}>        <Text style={{ textAlign: 'center', color: '#111', textDecorationLine: 'underline' }}>Abrir configurações</Text>      </Pressable>    </View>  );}export function CameraCaptureScreen() {  const cameraRef = useRef(null);  const [permission, requestPermission] = useCameraPermissions();  const [mediaPermission, requestMediaPermission] = MediaLibrary.usePermissions();  const [photoUri, setPhotoUri] = useState(null);  const [isCapturing, setIsCapturing] = useState(false);  const [isSaving, setIsSaving] = useState(false);  const [error, setError] = useState(null);  const openSettings = async () => {    try {      await Linking.openSettings();    } catch (e) {      setError('Não foi possível abrir as configurações.');    }  };  const ensurePermissions = async () => {    setError(null);    const cam = await requestPermission();    if (!cam?.granted) return false;    const med = await requestMediaPermission();    if (!med?.granted) return false;    return true;  };  const takePhoto = async () => {    setError(null);    try {      const ok = await ensurePermissions();      if (!ok) {        setError('Permissões necessárias não concedidas. Você pode continuar sem tirar foto.');        return;      }      if (!cameraRef.current) {        setError('Câmera indisponível no momento.');        return;      }      setIsCapturing(true);      const photo = await cameraRef.current.takePictureAsync({ quality: 0.8, skipProcessing: true });      setPhotoUri(photo.uri);    } catch (e) {      setError('Falha ao capturar a foto. Tente novamente.');    } finally {      setIsCapturing(false);    }  };  const savePhoto = async () => {    if (!photoUri) return;    setError(null);    try {      setIsSaving(true);      await MediaLibrary.createAssetAsync(photoUri);    } catch (e) {      setError('Não foi possível salvar a foto na galeria. Verifique permissões.');    } finally {      setIsSaving(false);    }  };  if (!permission) {    return <View style={{ padding: 16 }}><Text>Carregando permissões...</Text></View>;  }  if (!permission.granted) {    return (      <PermissionBlock        title="Acesso à câmera"        description="Precisamos da câmera para você capturar uma foto com preview e salvar no dispositivo."        onRequest={ensurePermissions}        onOpenSettings={openSettings}        isLoading={isCapturing}      />    );  }  return (    <View style={{ flex: 1, backgroundColor: '#000' }}>      <View style={{ flex: 1 }}>        {photoUri ? (          <Image source={{ uri: photoUri }} style={{ flex: 1 }} resizeMode="contain" />        ) : (          <CameraView ref={cameraRef} style={{ flex: 1 }} facing="back" />        )}      </View>      {!!error && (        <View style={{ padding: 12, backgroundColor: '#fff' }}>          <Text style={{ color: '#b00020' }}>{error}</Text>        </View>      )}      <View style={{ padding: 12, gap: 10, backgroundColor: '#111' }}>        {!photoUri ? (          <Pressable onPress={takePhoto} disabled={isCapturing} style={{ backgroundColor: '#fff', padding: 14, borderRadius: 10 }}>            <Text style={{ textAlign: 'center', fontWeight: '600' }}>{isCapturing ? 'Capturando...' : 'Tirar foto'}</Text>          </Pressable>        ) : (          <>            <Pressable onPress={() => setPhotoUri(null)} style={{ backgroundColor: '#333', padding: 14, borderRadius: 10 }}>              <Text style={{ textAlign: 'center', color: '#fff' }}>Refazer</Text>            </Pressable>            <Pressable onPress={savePhoto} disabled={isSaving} style={{ backgroundColor: '#fff', padding: 14, borderRadius: 10 }}>              <Text style={{ textAlign: 'center', fontWeight: '600' }}>{isSaving ? 'Salvando...' : 'Salvar na galeria'}</Text>            </Pressable>          </>        )}      </View>    </View>  );}

2) Pontos importantes do fluxo

  • Preview antes de salvar: guardamos photoUri e renderizamos <Image /> para o usuário confirmar.
  • Permissão de mídia separada: salvar na galeria pode exigir permissão adicional (especialmente no iOS).
  • Fallback: se o usuário negar, mostramos explicação e opção de abrir configurações; também é válido permitir seguir no app sem foto.
  • Erros: mensagens para o usuário devem ser claras e acionáveis; detalhes técnicos podem ir para logs internos.

Implementação prática: localização com precisão, atualização em tempo real e reverse geocoding

1) Solicitar permissão e obter posição atual

Para localização, você deve decidir o nível de precisão e o impacto em bateria. Para “posição atual” pontual, use alta precisão quando necessário (ex.: check-in) e menor precisão quando não for crítico.

import React, { useEffect, useMemo, useRef, useState } from 'react';import { View, Text, Pressable, ActivityIndicator, Linking } from 'react-native';import * as Location from 'expo-location';function formatCoords(coords) {  return {    latitude: coords.latitude,    longitude: coords.longitude,    accuracy: coords.accuracy,    altitude: coords.altitude,    heading: coords.heading,    speed: coords.speed,  };}export function LocationScreen() {  const [permissionStatus, setPermissionStatus] = useState('undetermined');  const [isRequesting, setIsRequesting] = useState(false);  const [isFetching, setIsFetching] = useState(false);  const [error, setError] = useState(null);  const [location, setLocation] = useState(null);  const [address, setAddress] = useState(null);  const watchRef = useRef(null);  const openSettings = async () => {    try {      await Linking.openSettings();    } catch (e) {      setError('Não foi possível abrir as configurações.');    }  };  const requestPermission = async () => {    setError(null);    setIsRequesting(true);    try {      const { status } = await Location.requestForegroundPermissionsAsync();      setPermissionStatus(status);      return status === 'granted';    } catch (e) {      setError('Falha ao solicitar permissão de localização.');      return false;    } finally {      setIsRequesting(false);    }  };  const getCurrentPosition = async () => {    setError(null);    setIsFetching(true);    try {      const servicesEnabled = await Location.hasServicesEnabledAsync();      if (!servicesEnabled) {        setError('Serviços de localização desativados. Ative o GPS para continuar.');        return;      }      const { status } = await Location.getForegroundPermissionsAsync();      setPermissionStatus(status);      if (status !== 'granted') {        setError('Permissão de localização não concedida.');        return;      }      const pos = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Highest });      setLocation({ coords: formatCoords(pos.coords), timestamp: pos.timestamp });    } catch (e) {      setError('Não foi possível obter sua localização.');    } finally {      setIsFetching(false);    }  };  const reverseGeocode = async (coords) => {    try {      const results = await Location.reverseGeocodeAsync({ latitude: coords.latitude, longitude: coords.longitude });      const first = results?.[0];      if (!first) return null;      const parts = [first.street, first.streetNumber, first.district, first.city, first.region, first.postalCode].filter(Boolean);      return parts.join(', ');    } catch (e) {      return null;    }  };  const startWatching = async () => {    setError(null);    try {      const ok = await requestPermission();      if (!ok) {        setError('Sem permissão, não é possível acompanhar sua localização em tempo real.');        return;      }      const servicesEnabled = await Location.hasServicesEnabledAsync();      if (!servicesEnabled) {        setError('Serviços de localização desativados.');        return;      }      if (watchRef.current) return;      watchRef.current = await Location.watchPositionAsync(        {          accuracy: Location.Accuracy.High,          timeInterval: 2000,          distanceInterval: 5,        },        async (pos) => {          const next = { coords: formatCoords(pos.coords), timestamp: pos.timestamp };          setLocation(next);          const addr = await reverseGeocode(next.coords);          setAddress(addr);        }      );    } catch (e) {      setError('Falha ao iniciar rastreamento.');    }  };  const stopWatching = () => {    if (watchRef.current) {      watchRef.current.remove();      watchRef.current = null;    }  };  useEffect(() => {    return () => stopWatching();  }, []);  return (    <View style={{ flex: 1, padding: 16, gap: 12 }}>      <Text style={{ fontSize: 18, fontWeight: '600' }}>Localização</Text>      <Text style={{ color: '#444' }}>Permissão: {permissionStatus}</Text>      {!!error && (        <View style={{ padding: 12, backgroundColor: '#fde7e9', borderRadius: 8 }}>          <Text style={{ color: '#b00020' }}>{error}</Text>          <Pressable onPress={openSettings} style={{ marginTop: 8 }}>            <Text style={{ textDecorationLine: 'underline' }}>Abrir configurações</Text>          </Pressable>        </View>      )}      <Pressable onPress={requestPermission} style={{ backgroundColor: '#111', padding: 12, borderRadius: 8 }}>        {isRequesting ? <ActivityIndicator color="#fff" /> : <Text style={{ color: '#fff', textAlign: 'center' }}>Solicitar permissão</Text>}      </Pressable>      <Pressable onPress={getCurrentPosition} style={{ backgroundColor: '#111', padding: 12, borderRadius: 8 }}>        {isFetching ? <ActivityIndicator color="#fff" /> : <Text style={{ color: '#fff', textAlign: 'center' }}>Obter localização atual</Text>}      </Pressable>      <View style={{ flexDirection: 'row', gap: 10 }}>        <Pressable onPress={startWatching} style={{ flex: 1, backgroundColor: '#111', padding: 12, borderRadius: 8 }}>          <Text style={{ color: '#fff', textAlign: 'center' }}>Iniciar tempo real</Text>        </Pressable>        <Pressable onPress={stopWatching} style={{ flex: 1, backgroundColor: '#333', padding: 12, borderRadius: 8 }}>          <Text style={{ color: '#fff', textAlign: 'center' }}>Parar</Text>        </Pressable>      </View>      <View style={{ padding: 12, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, gap: 6 }}>        <Text style={{ fontWeight: '600' }}>Dados</Text>        {location ? (          <>            <Text>Lat: {location.coords.latitude}</Text>            <Text>Lng: {location.coords.longitude}</Text>            <Text>Precisão (m): {location.coords.accuracy ?? 'n/a'}</Text>            <Text>Timestamp: {new Date(location.timestamp).toLocaleString()}</Text>            <Text>Endereço (opcional): {address ?? '—'}</Text>          </>        ) : (          <Text style={{ color: '#666' }}>Nenhuma localização obtida ainda.</Text>        )}      </View>    </View>  );}

2) Ajustando precisão, consumo e frequência

ObjetivoConfiguração sugeridaObservação
Localização pontual (check-in)getCurrentPositionAsync({ accuracy: Highest })Maior consumo, use apenas quando necessário
Tracking levewatchPositionAsync com accuracy: Balanced, distanceInterval maiorMenos atualizações e melhor bateria
Tracking em tempo realaccuracy: High, timeInterval curto, distanceInterval pequenoUse por períodos curtos e com indicação clara ao usuário

Evite manter rastreamento ativo sem necessidade. Sempre forneça um botão “Parar” e pare automaticamente ao sair da tela (cleanup no useEffect).

Fluxos de aceite/recusa e fallbacks (câmera e localização)

Estados recomendados e mensagens

  • Undetermined: “Para usar este recurso, precisamos da sua permissão.”
  • Denied: “Sem permissão, não conseguimos acessar X. Você pode habilitar em Configurações.”
  • Services disabled (localização): “Ative o GPS/serviços de localização para continuar.”
  • Hardware indisponível: “Recurso não disponível neste dispositivo.” (ex.: emulador sem câmera funcional)
  • Erro inesperado: “Algo deu errado. Tente novamente.”

Fallbacks práticos

  • Sem câmera: permitir anexar uma imagem já existente (se você optar por implementar seleção de galeria em outra etapa) ou seguir sem foto.
  • Sem localização: permitir digitar endereço manualmente ou escolher um ponto em mapa (se houver).
  • Reverse geocoding falhou: mostrar apenas coordenadas e permitir “Tentar novamente”.

Privacidade e boas práticas de compliance

Minimização de dados

  • Peça apenas o necessário: localização “quando em uso” em vez de “sempre”, se o app não precisa de background.
  • Evite armazenar localização precisa sem necessidade. Se for armazenar, defina retenção e finalidade.
  • Para fotos, deixe claro se serão salvas no dispositivo, enviadas para servidor, ou ambas.

Mensagens de permissão claras (exemplos)

  • Câmera: “Usamos a câmera para você tirar uma foto do documento e revisar antes de salvar.”
  • Localização: “Usamos sua localização apenas enquanto você estiver nesta tela para mostrar pontos próximos.”
  • Galeria: “Precisamos salvar a foto na sua galeria quando você confirmar.”

Tratamento de erros sem vazar dados

  • Não exiba detalhes técnicos (stack trace) ao usuário.
  • Se registrar logs, evite incluir coordenadas completas e URIs sensíveis em ambientes de produção.
  • Mostre ações: “Tentar novamente”, “Abrir configurações”, “Continuar sem”.

Checklist de qualidade antes de considerar pronto

  • Permissões solicitadas apenas ao iniciar a ação.
  • Fluxo de recusa testado (negar uma vez e negar permanentemente no Android).
  • Serviços de localização desativados tratados com mensagem e orientação.
  • Preview de foto funciona e permite refazer.
  • Salvamento local tratado com permissão e erro.
  • Tracking em tempo real tem botão de parar e cleanup ao desmontar.
  • Reverse geocoding é opcional e falha de forma silenciosa (sem quebrar a tela).

Agora responda o exercício sobre o conteúdo:

Ao implementar câmera e localização em um app React Native com Expo, qual abordagem melhor equilibra experiência do usuário, permissões e fallbacks?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A boa prática é pedir permissão quando o usuário aciona o recurso, explicar a finalidade e tratar recusa, recusa permanente, serviços desativados e indisponibilidade, oferecendo fallbacks como abrir configurações ou seguir sem câmera/localização.

Próximo capitúlo

Performance em React Native: listas, imagens, memoização e perfilamento

Arrow Right Icon
Capa do Ebook gratuito React Native Essencial: criando apps completos com JavaScript e boas práticas
75%

React Native Essencial: criando apps completos com JavaScript e boas práticas

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.