Gestão de Perfil Completa no React Native com Firebase (Auth, Firestore, Storage) e AsyncStorage

Em aplicativos móveis modernos, a persistência de dados e a experiência do usuário (UX) são cruciais. Este artigo analisa o componente TelaPerfil, uma solução robusta em React Native com Firebase para gerenciar perfis de usuário. O código implementa um fluxo completo: autenticação, armazenamento de dados textuais no Firestore, upload de foto no Firebase Storage, e uma estratégia de cache local com AsyncStorage para carregamento instantâneo.

1. Arquitetura e Estratégia de Carregamento Híbrido (Cache + Nuvem)

image-2 Gestão de Perfil Completa no React Native com Firebase (Auth, Firestore, Storage) e AsyncStorage

O componente utiliza o hook useEffect para gerenciar o ciclo de carregamento de dados assim que a tela é montada. A grande vantagem deste código é a sua estratégia de carregamento híbrido para otimizar a UX:

Etapa 1: Carregamento Instantâneo (AsyncStorage)

Antes de fazer qualquer requisição à rede, o código tenta ler os dados do perfil salvos localmente no dispositivo usando o AsyncStorage:

JavaScript

const perfilLocal = await AsyncStorage.getItem(storageKey);
if (perfilLocal) {
  const dadosLocais = JSON.parse(perfilLocal);
  setPerfil({ ...dadosLocais }); // Preenche o estado instantaneamente
  setPhotoUrl(dadosLocais.fotoUrl || null);
  setCarregando(false); // Oculta o spinner inicial
}

Isso garante que o usuário veja seus dados imediatamente, mesmo sem conexão com a internet, eliminando a percepção de lentidão.

Etapa 2: Atualização em Segundo Plano (Firestore)

Após exibir os dados locais, o código faz uma requisição assíncrona ao Firestore para obter os dados mais recentes:

JavaScript

const perfilRef = doc(bancoDados, 'users', usuario.uid);
const perfilSnap = await getDoc(perfilRef);
if (perfilSnap.exists()) {
  const dados = perfilSnap.data();
  // ... processa os dados ...
  setPerfil({ ...novosDados }); // Atualiza o estado com dados da nuvem
  await AsyncStorage.setItem(storageKey, JSON.stringify(novosDados)); // Atualiza o cache local
}

Se houver diferenças entre os dados locais e os da nuvem, a interface é atualizada silenciosamente, e o cache local é renovado.

2. Gestão da Foto de Perfil (ImagePicker + Storage)

A manipulação da imagem de perfil envolve a interação com a galeria do dispositivo e o upload para a nuvem.

Seleção de Imagem (ImagePicker)

A função selecionarFoto utiliza a biblioteca expo-image-picker:

  1. Solicita permissão de acesso à galeria.
  2. Abre a interface da galeria para o usuário escolher uma imagem.
  3. Permite edição (recorte) e define a qualidade para 0.7 (para otimizar o tamanho do arquivo).
  4. Salva a URI da imagem local no estado localImage para exibição prévia na interface.

Upload e Obtenção da URL (Storage)

O upload real para o Firebase Storage ocorre na função uploadImageAsync, chamada apenas quando o usuário salva o perfil:

JavaScript

const uploadImageAsync = async (uri) => {
  const response = await fetch(uri); // Converte a URI local em um blob
  const blob = await response.blob();
  const imagemRef = ref(armazenamento, `profilePictures/${usuario.uid}/${Date.now()}`); // Referência única
  const snapshot = await uploadBytes(imagemRef, blob); // Faz o upload dos bytes
  return await getDownloadURL(snapshot.ref); // Retorna a URL pública gerada
};

Esta função transforma a imagem local em dados binários (blob) e os envia para uma pasta específica do usuário no Storage, retornando a URL pública necessária para exibir a imagem posteriormente.

3. Salvamento Consolidador do Perfil

A função salvarPerfil coordena a persistência dos dados em três locais diferentes:

  1. Firebase Storage: Se uma nova imagem local foi selecionada, faz o upload e obtém a nova URL.
  2. Firebase Auth (Profile): Atualiza o displayName (nome completo) e o photoURL no objeto de autenticação do usuário, garantindo que essas informações básicas estejam disponíveis rapidamente em todo o app.
  3. Firebase Firestore: Salva o objeto completo do perfil (endereço, telefone, URL da foto, data de atualização) na coleção 'users', usando setDoc com { merge: true } para criar ou atualizar o documento sem sobrescrever campos não enviados.
  4. AsyncStorage: Atualiza o cache local com os dados finais salvos, garantindo a consistência para o próximo carregamento.

4. Recursos de Interface e UX

O componente implementa padrões de design e interações que melhoram a experiência do usuário:

  • Feedback de Carregamento: Utiliza ActivityIndicator tanto no carregamento inicial da tela quanto no estado de salvamento (salvando), desativando o botão para evitar cliques duplos.
  • Modo de Edição: O estado editando controla a propriedade editable dos TextInput e a visibilidade do botão “Alterar Foto”, alternando entre visualização e edição de forma limpa.
  • Scrollview: O conteúdo é envolto em um ScrollView para garantir que todos os campos do formulário estejam acessíveis em telas de diferentes tamanhos e quando o teclado estiver aberto.
  • Tipos de Teclado: Configura keyboardType="numeric" para o CEP e keyboardType="phone-pad" para o telefone, facilitando a digitação correta pelo usuário.

5. 📚 Glossário de Comandos Utilizados

⚛️ React & React Native

  • AsyncStorage: Sistema de armazenamento assíncrono, persistente e chave-valor, usado para cache local no dispositivo.
  • ImagePicker (Expo): Biblioteca para acessar a câmera e a galeria de fotos do dispositivo.
  • ScrollView: Componente de container que permite rolar o conteúdo quando ele não cabe na tela.

🔥 Firebase (Auth, Firestore, Storage)

  • autenticacao.currentUser: Obtém o objeto do usuário atualmente logado.
  • updateProfile() (Auth): Atualiza as informações básicas do perfil do usuário autenticado (displayName, photoURL).
  • getDoc() (Firestore): Lê um documento específico do banco de dados.
  • setDoc() (Firestore): Cria ou substitui um documento. Com { merge: true }, ele atualiza apenas os campos fornecidos.
  • ref() (Storage): Cria uma referência a um local no Firebase Storage para upload ou download.
  • uploadBytes() (Storage): Faz o upload de dados binários (blobs) para o local referenciado.
  • getDownloadURL() (Storage): Obtém a URL pública e persistente de um arquivo armazenado no Storage.

Prompt Sugerido (Toque para selecionar)
import React, { useEffect, useState } from 'react'; import { View, Text, TextInput, Button, Image, StyleSheet, ScrollView, ActivityIndicator, Alert, } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { autenticacao, bancoDados, armazenamento } from '../config/firebaseConfig'; import { doc, getDoc, setDoc } from 'firebase/firestore'; import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; import { updateProfile } from 'firebase/auth'; import AsyncStorage from '@react-native-async-storage/async-storage';const camposIniciais = { nome: '', sobrenome: '', rua: '', bairro: '', cidade: '', estado: '', cep: '', telefone: '', };export default function TelaPerfil() { const [perfil, setPerfil] = useState(camposIniciais); const [photoUrl, setPhotoUrl] = useState(null); const [localImage, setLocalImage] = useState(null); const [editando, setEditando] = useState(false); const [carregando, setCarregando] = useState(true); const [salvando, setSalvando] = useState(false);const usuario = autenticacao.currentUser;useEffect(() => { const carregarDados = async () => { if (!usuario) { setCarregando(false); return; }const storageKey = `@perfil_usuario_${usuario.uid}`; // 1. Tentar ler do AsyncStorage primeiro para exibição instantânea try { const perfilLocal = await AsyncStorage.getItem(storageKey); if (perfilLocal) { const dadosLocais = JSON.parse(perfilLocal); setPerfil({ nome: dadosLocais.nome || '', sobrenome: dadosLocais.sobrenome || '', rua: dadosLocais.rua || '', bairro: dadosLocais.bairro || '', cidade: dadosLocais.cidade || '', estado: dadosLocais.estado || '', cep: dadosLocais.cep || '', telefone: dadosLocais.telefone || '', }); setPhotoUrl(dadosLocais.fotoUrl || null); setEditando(false); // Ocultar o carregando inicial caso já tenhamos dados locais setCarregando(false); } } catch (e) { console.error("Erro ao carregar dados locais do perfil:", e); }// 2. Fazer requisição ao Firestore em segundo plano para obter os dados mais atualizados try { const perfilRef = doc(bancoDados, 'users', usuario.uid); const perfilSnap = await getDoc(perfilRef);if (perfilSnap.exists()) { const dados = perfilSnap.data(); const novosDados = { nome: dados.nome || '', sobrenome: dados.sobrenome || '', rua: dados.rua || '', bairro: dados.bairro || '', cidade: dados.cidade || '', estado: dados.estado || '', cep: dados.cep || '', telefone: dados.telefone || '', fotoUrl: dados.fotoUrl || usuario.photoURL || null, }; setPerfil({ nome: novosDados.nome, sobrenome: novosDados.sobrenome, rua: novosDados.rua, bairro: novosDados.bairro, cidade: novosDados.cidade, estado: novosDados.estado, cep: novosDados.cep, telefone: novosDados.telefone, }); setPhotoUrl(novosDados.fotoUrl); setEditando(false);// Atualizar o cache local no AsyncStorage await AsyncStorage.setItem(storageKey, JSON.stringify(novosDados)); } else { const [primeiroNome, ...rest] = (usuario.displayName || '').split(' '); const dadosPadrao = { nome: primeiroNome || '', sobrenome: rest.join(' ') || '', rua: '', bairro: '', cidade: '', estado: '', cep: '', telefone: '', fotoUrl: usuario.photoURL || null, };setPerfil({ nome: dadosPadrao.nome, sobrenome: dadosPadrao.sobrenome, rua: dadosPadrao.rua, bairro: dadosPadrao.bairro, cidade: dadosPadrao.cidade, estado: dadosPadrao.estado, cep: dadosPadrao.cep, telefone: dadosPadrao.telefone, }); setPhotoUrl(dadosPadrao.fotoUrl); setEditando(true);// Salvar dados padrão no cache local do AsyncStorage await AsyncStorage.setItem(storageKey, JSON.stringify(dadosPadrao)); } } catch (erro) { console.error("Erro ao buscar dados do Firestore:", erro); // Exibir erro apenas se não conseguimos carregar nada localmente const perfilLocal = await AsyncStorage.getItem(storageKey); if (!perfilLocal) { Alert.alert('Erro', 'Não foi possível carregar os dados do perfil.'); } } finally { setCarregando(false); } };carregarDados(); }, [usuario]);const selecionarFoto = async () => { const permissao = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (permissao.status !== 'granted') { Alert.alert( 'Permissão necessária', 'Permita o acesso à galeria para escolher a foto de perfil.' ); return; }const resultado = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, quality: 0.7, });if (!resultado.canceled && resultado.assets?.length > 0) { setLocalImage(resultado.assets[0].uri); } else if (!resultado.cancelled && resultado.uri) { setLocalImage(resultado.uri); } };const uploadImageAsync = async (uri) => { const response = await fetch(uri); const blob = await response.blob(); const imagemRef = ref(armazenamento, `profilePictures/${usuario.uid}/${Date.now()}`); const snapshot = await uploadBytes(imagemRef, blob); return await getDownloadURL(snapshot.ref); };const salvarPerfil = async () => { if (!usuario) { return; }setSalvando(true); try { let uploadedUrl = photoUrl;if (localImage) { uploadedUrl = await uploadImageAsync(localImage); }const nomeCompleto = `${perfil.nome.trim()} ${perfil.sobrenome.trim()}`.trim(); const usuarioAtualizado = {}; if (nomeCompleto) usuarioAtualizado.displayName = nomeCompleto; if (uploadedUrl) usuarioAtualizado.photoURL = uploadedUrl;if (Object.keys(usuarioAtualizado).length > 0) { await updateProfile(usuario, usuarioAtualizado); }const perfilRef = doc(bancoDados, 'users', usuario.uid); const novosDados = { nome: perfil.nome, sobrenome: perfil.sobrenome, rua: perfil.rua, bairro: perfil.bairro, cidade: perfil.cidade, estado: perfil.estado, cep: perfil.cep, telefone: perfil.telefone, fotoUrl: uploadedUrl || null, updatedAt: new Date(), };await setDoc(perfilRef, novosDados, { merge: true });// Salvar os novos dados no AsyncStorage localmente também const storageKey = `@perfil_usuario_${usuario.uid}`; await AsyncStorage.setItem(storageKey, JSON.stringify(novosDados));setPhotoUrl(uploadedUrl); setLocalImage(null); setEditando(false); Alert.alert('Sucesso', 'Perfil atualizado com sucesso.'); } catch (erro) { Alert.alert('Erro', 'Não foi possível salvar o perfil. Tente novamente.'); } finally { setSalvando(false); } };const atualizarCampo = (campo, valor) => { setPerfil((anterior) => ({ ...anterior, [campo]: valor })); };if (carregando) { return ( <View style={estilos.centralizado}> <ActivityIndicator size="large" /> </View> ); }return ( <ScrollView contentContainerStyle={estilos.container}> <Text style={estilos.titulo}>Perfil do Usuário</Text> <View style={estilos.avatarContainer}> {localImage ? ( <Image source={{ uri: localImage }} style={estilos.avatar} /> ) : photoUrl ? ( <Image source={{ uri: photoUrl }} style={estilos.avatar} /> ) : ( <View style={[estilos.avatar, estilos.avatarVazio]}> <Text style={estilos.avatarTexto}>Foto</Text> </View> )} </View>{editando && ( <Button title="Alterar Foto" onPress={selecionarFoto} /> )}<Text>Nome</Text> <TextInput style={estilos.input} value={perfil.nome} onChangeText={(valor) => atualizarCampo('nome', valor)} editable={editando} /><Text>Sobrenome</Text> <TextInput style={estilos.input} value={perfil.sobrenome} onChangeText={(valor) => atualizarCampo('sobrenome', valor)} editable={editando} /><Text>Rua</Text> <TextInput style={estilos.input} value={perfil.rua} onChangeText={(valor) => atualizarCampo('rua', valor)} editable={editando} /><Text>Bairro</Text> <TextInput style={estilos.input} value={perfil.bairro} onChangeText={(valor) => atualizarCampo('bairro', valor)} editable={editando} /><Text>Cidade</Text> <TextInput style={estilos.input} value={perfil.cidade} onChangeText={(valor) => atualizarCampo('cidade', valor)} editable={editando} /><Text>Estado</Text> <TextInput style={estilos.input} value={perfil.estado} onChangeText={(valor) => atualizarCampo('estado', valor)} editable={editando} /><Text>CEP</Text> <TextInput style={estilos.input} value={perfil.cep} onChangeText={(valor) => atualizarCampo('cep', valor)} editable={editando} keyboardType="numeric" /><Text>Telefone celular</Text> <TextInput style={estilos.input} value={perfil.telefone} onChangeText={(valor) => atualizarCampo('telefone', valor)} editable={editando} keyboardType="phone-pad" />{editando ? ( <> <Button title={salvando ? 'Salvando...' : 'Salvar Perfil'} onPress={salvarPerfil} disabled={salvando} /> <View style={estilos.espaco} /> <Button title="Cancelar" onPress={() => setEditando(false)} /> </> ) : ( <Button title="Editar Perfil" onPress={() => setEditando(true)} /> )} </ScrollView> ); }const estilos = StyleSheet.create({ container: { padding: 20, }, titulo: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, textAlign: 'center', }, input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, marginBottom: 12, padding: 10, }, avatarContainer: { alignItems: 'center', marginBottom: 20, }, avatar: { width: 130, height: 130, borderRadius: 65, marginBottom: 12, }, avatarVazio: { backgroundColor: '#ddd', justifyContent: 'center', alignItems: 'center', }, avatarTexto: { color: '#555', fontSize: 16, }, centralizado: { flex: 1, justifyContent: 'center', alignItems: 'center', }, espaco: { height: 10, }, });

Painel de Administração em Tempo Real CRUD com React Native e Firebase Firestore

Compartilhe:

Profissional engajado com as últimas tendências tecnológicas e de gestão, buscando continuamente aprimorar suas competências e compartilhar seu conhecimento.

Publicar comentário