Criando uma Tela de Perfil Completa no React Native com Firebase, AsyncStorage e Upload de Foto

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ção
  • useEffect → 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:

  1. Primeiro carrega dados locais do AsyncStorage
  2. 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 documento
  • getDoc() → 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:

  1. Dados locais
  2. 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:

  • displayName
  • photoURL

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

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 { 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, }, });

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