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:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
npx expo install expo-camera expo-media-library expo-locationSe 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|deniedisRequestingPermission: evita múltiplos promptserror: mensagem amigável e log técnico separadophotoUri: URI da foto capturada para previewisSaving: salvamento em andamentolocation: 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
photoUrie 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
| Objetivo | Configuração sugerida | Observação |
|---|---|---|
| Localização pontual (check-in) | getCurrentPositionAsync({ accuracy: Highest }) | Maior consumo, use apenas quando necessário |
| Tracking leve | watchPositionAsync com accuracy: Balanced, distanceInterval maior | Menos atualizações e melhor bateria |
| Tracking em tempo real | accuracy: High, timeInterval curto, distanceInterval pequeno | Use 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).