Desenvolver uma tela de perfil em aplicações mobile é uma das tarefas mais importantes em projetos reais. Nesse exemplo, temos uma implementação bastante profissional utilizando:
O código possui recursos modernos como:
- Cache local com AsyncStorage
- Atualização automática de cache
- Carregamento híbrido (local + nuvem)
- Upload de imagem
- Edição de dados
- Persistência offline
- Sincronização com Firebase
Neste artigo vamos analisar os principais pontos técnicos dessa implementação.
Estrutura Inicial do Projeto
Logo no início do código temos os imports principais:
import React, { useEffect, useState } from 'react';
Aqui estamos utilizando:
useState → controlar estados da aplicaçãouseEffect → executar carregamentos automáticos
Também temos bibliotecas importantes:
import * as ImagePicker from 'expo-image-picker';
Responsável por acessar a galeria do celular.
import AsyncStorage from '@react-native-async-storage/async-storage';
Responsável pelo armazenamento local no dispositivo.
Estrutura Inicial dos Campos
const camposIniciais = {
nome: '',
sobrenome: '',
rua: '',
bairro: '',
cidade: '',
estado: '',
cep: '',
telefone: '',
};
Esse objeto funciona como um modelo padrão do perfil do usuário.
Isso evita:
- valores
undefined - erros em inputs
- inconsistências na interface
Estados da Aplicação
O componente utiliza diversos estados:
const [perfil, setPerfil] = useState(camposIniciais);
Armazena os dados do perfil.
const [photoUrl, setPhotoUrl] = useState(null);
Armazena a URL da foto salva no Firebase.
const [localImage, setLocalImage] = useState(null);
Armazena temporariamente a imagem escolhida no dispositivo.
const [editando, setEditando] = useState(false);
Controla o modo de edição.
const [carregando, setCarregando] = useState(true);
Controla o loading inicial.
const [salvando, setSalvando] = useState(false);
Controla o loading durante o salvamento.
Estratégia Inteligente de Carregamento
Um dos pontos mais importantes do código é a estratégia híbrida:
- Primeiro carrega dados locais do AsyncStorage
- Depois busca dados atualizados no Firebase
Essa técnica melhora MUITO a experiência do usuário.
Carregando Dados Locais Instantaneamente
const perfilLocal = await AsyncStorage.getItem(storageKey);
Aqui o aplicativo busca os dados salvos localmente.
Se existirem:
const dadosLocais = JSON.parse(perfilLocal);
Os dados são convertidos novamente para objeto JavaScript.
Atualizando a Interface Imediatamente
setPerfil({
nome: dadosLocais.nome || '',
sobrenome: dadosLocais.sobrenome || '',
});
A tela é preenchida instantaneamente.
Isso é extremamente importante porque:
- evita tela branca
- reduz sensação de lentidão
- melhora UX
- funciona offline
Cache Local com AsyncStorage
O AsyncStorage funciona como um banco local simples.
Os dados são salvos usando:
await AsyncStorage.setItem(storageKey, JSON.stringify(novosDados));
Observe:
JSON.stringify()
O AsyncStorage salva apenas strings.
Por isso precisamos converter objetos para JSON.
Chave Dinâmica do Usuário
const storageKey = `@perfil_usuario_${usuario.uid}`;
Essa estratégia evita conflitos entre usuários diferentes.
Cada usuário possui seu próprio cache local.
Exemplo:
@perfil_usuario_abc123
@perfil_usuario_xyz456
Buscando Dados Atualizados no Firestore
Após carregar os dados locais, o sistema faz uma requisição ao Firebase:
const perfilRef = doc(bancoDados, 'users', usuario.uid);
const perfilSnap = await getDoc(perfilRef);
Aqui:
doc() → referencia o documentogetDoc() → busca os dados
Atualização Automática de Cache
Depois da resposta do Firebase:
await AsyncStorage.setItem(storageKey, JSON.stringify(novosDados));
O cache local é atualizado automaticamente.
Esse processo é fundamental porque:
- mantém dados sincronizados
- reduz consultas futuras
- melhora performance
- permite uso offline
Estratégia Offline First
Esse código segue parcialmente o conceito:
Offline First
A aplicação prioriza:
- Dados locais
- Depois sincronização online
Essa abordagem é muito utilizada em:
- Instagram
- WhatsApp
- Spotify
- aplicativos bancários
Tratamento de Erros
catch (erro) {
console.error("Erro ao buscar dados do Firestore:", erro);
}
O código evita quebra da aplicação.
Além disso:
Alert.alert('Erro', 'Não foi possível carregar os dados do perfil.');
Mostra feedback visual ao usuário.
Seleção de Foto da Galeria
O upload começa com a permissão:
const permissao = await ImagePicker.requestMediaLibraryPermissionsAsync();
Aplicativos mobile precisam pedir autorização do usuário.
Abrindo a Galeria
const resultado = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 0.7,
});
Aqui temos:
- seleção apenas de imagens
- corte/edição habilitado
- compressão da qualidade
Preview Instantâneo da Foto
setLocalImage(resultado.assets[0].uri);
A imagem é exibida imediatamente na interface.
Mesmo antes do upload.
Isso melhora bastante a experiência do usuário.
Processo de Upload da Imagem
A função:
const uploadImageAsync = async (uri)
é responsável pelo envio da imagem.
Convertendo Imagem em Blob
const response = await fetch(uri);
const blob = await response.blob();
O Firebase Storage trabalha com arquivos binários.
Por isso convertemos a imagem para Blob.
Enviando para o Firebase Storage
const imagemRef = ref(
armazenamento,
`profilePictures/${usuario.uid}/${Date.now()}`
);
Aqui criamos um caminho único para a imagem.
O Date.now() evita sobrescrever arquivos antigos.
Upload do Arquivo
const snapshot = await uploadBytes(imagemRef, blob);
Realiza efetivamente o upload.
Obtendo URL Pública da Imagem
return await getDownloadURL(snapshot.ref);
Após upload, recebemos a URL da imagem.
Essa URL será salva no perfil do usuário.
Atualizando Perfil do Firebase Authentication
await updateProfile(usuario, usuarioAtualizado);
Essa função atualiza:
do usuário autenticado.
Salvando Dados no Firestore
await setDoc(perfilRef, novosDados, { merge: true });
O parâmetro:
{ merge: true }
é extremamente importante.
Ele evita apagar outros campos existentes.
Atualização de Campos
const atualizarCampo = (campo, valor) => {
setPerfil((anterior) => ({
...anterior,
[campo]: valor
}));
};
Essa técnica usa:
...anterior
para preservar os demais campos do objeto.
É uma abordagem moderna e segura.
Controle de Loading
Enquanto os dados carregam:
if (carregando)
o usuário visualiza:
<ActivityIndicator size="large" />
Isso melhora muito a experiência visual.
Controle de Edição
O sistema alterna entre:
- modo visualização
- modo edição
através do estado:
editando
Quando:
editable={editando}
os inputs ficam bloqueados ou liberados.
Salvamento Seguro
Durante o salvamento:
disabled={salvando}
impede múltiplos cliques no botão.
Isso evita:
- duplicidade
- múltiplos uploads
- corrupção de dados
Organização do Código
Esse componente apresenta boas práticas importantes:
Separação de responsabilidades
Funções específicas para:
- carregar dados
- salvar perfil
- upload de imagem
- atualizar campos
Experiência do Usuário
O código demonstra várias preocupações com UX:
- carregamento rápido
- preview da imagem
- cache local
- funcionamento offline
- loading indicators
- tratamento de erros
Código Completo
import React, { useEffect, useState } from 'react';
import {
View,
Text,
TextInput,
Button,
Image,
StyleSheet,
ScrollView,
ActivityIndicator,
Alert,
} from 'react-native';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,
},
});
Publicar comentário