Visão do projeto (o que vamos construir)
Neste capítulo, você vai consolidar tudo em um app completo com fluxo de autenticação, múltiplas telas, navegação combinada (Stack + Tabs), consumo de API, formulários validados, persistência local e um recurso do dispositivo (câmera ou localização). A proposta é trabalhar como em um projeto real: definir requisitos, criar critérios de aceite, dividir em etapas e aplicar organização de código desde o início.
App sugerido: “FieldTasks” (tarefas em campo). Usuários autenticam, visualizam tarefas, criam/atualizam tarefas com formulário validado, anexam foto (câmera) ou registram localização, e o app mantém cache local para uso offline parcial.
Escopo funcional (requisitos)
- Autenticação: login, logout, persistência de sessão (token), proteção de rotas.
- Navegação: Stack para fluxos (Auth, detalhes) + Tabs para áreas principais (Home/Tarefas, Criar, Perfil).
- API: listar tarefas, ver detalhes, criar/editar tarefa, atualizar status.
- Formulários: validação (campos obrigatórios, limites, feedback de erro), UX com loading e desabilitar botão.
- Persistência local: cache de lista/detalhes, fila simples de ações offline (opcional), preferências do usuário.
- Recurso do dispositivo: câmera (anexo de foto) ou localização (capturar coordenadas).
- Tratamento de erros: mensagens amigáveis, retry, fallback para cache.
Critérios de aceite (Definition of Done)
- Usuário consegue autenticar e permanecer logado após fechar/reabrir o app.
- Rotas privadas não aparecem sem autenticação; logout limpa dados sensíveis.
- Tabs funcionam e cada aba mantém seu estado ao alternar.
- Lista de tarefas carrega da API; em falha de rede, carrega do cache quando disponível.
- Formulário de tarefa valida campos e impede envio inválido; exibe erros por campo.
- Ao criar/editar tarefa, UI reflete a mudança (otimista ou após resposta) e trata falhas.
- Recurso do dispositivo funciona com permissões (solicita, trata negação, orienta usuário).
- Build Android e iOS geram sem erros, com variáveis de ambiente e configurações corretas.
Arquitetura e organização (desde o início)
Como os capítulos anteriores já cobriram conceitos e ferramentas, aqui o foco é como amarrar tudo em uma estrutura coesa. A ideia é separar responsabilidades: UI, navegação, estado global, serviços de API, armazenamento e recursos do dispositivo.
Estrutura de pastas sugerida
src/ app/ (bootstrap, providers, config) navigation/ (stacks, tabs, guards) screens/ (telas) auth/ tasks/ profile/ components/ (componentes reutilizáveis) features/ (domínios: tasks, auth) auth/ auth.store.ts auth.service.ts tasks/ tasks.store.ts tasks.service.ts tasks.cache.ts services/ (infra: http, storage, permissions) http/ client.ts interceptors.ts storage/ secure.ts async.ts utils/ (helpers: validators, formatters) types/ (tipos globais)Regra prática: telas não devem conhecer detalhes de HTTP/AsyncStorage. Elas chamam funções do domínio (services/stores) e renderizam estados.
Etapa 1 — Definir contrato de dados e endpoints
Antes de codar UI, defina os modelos e endpoints para evitar retrabalho. Exemplo de tipos:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
// src/features/tasks/tasks.types.tsexport type TaskStatus = 'open' | 'in_progress' | 'done';export type Task = { id: string; title: string; description?: string; status: TaskStatus; createdAt: string; updatedAt: string; photoUrl?: string; location?: { lat: number; lng: number };};export type CreateTaskInput = { title: string; description?: string; photoBase64?: string; location?: { lat: number; lng: number };};Endpoints (exemplo):
POST /auth/login→ retornatokeneuserGET /tasks→ lista tarefasGET /tasks/:id→ detalhesPOST /tasks→ cria tarefaPATCH /tasks/:id→ atualiza status/dados
Etapa 2 — Setup de providers e bootstrap do app
Centralize providers (estado global, tema, query/cache se usar) em um único ponto. Exemplo de composição:
// src/app/AppProviders.tsximport React from 'react';import { AuthProvider } from '../features/auth/auth.store';export function AppProviders({ children }: { children: React.ReactNode }) { return ( <AuthProvider> {children} </AuthProvider> );}O bootstrap decide qual árvore de navegação renderizar com base no estado de autenticação (token carregado do armazenamento seguro).
Etapa 3 — Navegação (Stack + Tabs) com rotas protegidas
Implemente três blocos:
- AuthStack: Login, Recuperar senha (opcional).
- MainTabs: Tarefas (lista), Criar tarefa, Perfil.
- RootStack: decide entre AuthStack e MainTabs; inclui telas modais/detalhes.
// src/navigation/RootNavigator.tsximport React from 'react';import { NavigationContainer } from '@react-navigation/native';import { useAuth } from '../features/auth/auth.store';import { AuthStack } from './stacks/AuthStack';import { AppStack } from './stacks/AppStack';export function RootNavigator() { const { status } = useAuth(); // status: 'loading' | 'signedOut' | 'signedIn' return ( <NavigationContainer> {status === 'signedIn' ? <AppStack /> : <AuthStack />} </NavigationContainer> );}Ponto de atenção: enquanto carrega token do storage, mostre uma tela de splash/loading para evitar “piscar” a tela de login.
Etapa 4 — Camada HTTP com interceptors e erros padronizados
Centralize o cliente HTTP e padronize erros para a UI não depender de detalhes de rede.
// src/services/http/client.tsimport axios from 'axios';import { getAuthToken } from '../storage/secure';export const http = axios.create({ baseURL: process.env.API_URL, timeout: 15000,});http.interceptors.request.use(async (config) => { const token = await getAuthToken(); if (token) config.headers.Authorization = `Bearer ${token}`; return config;});export type AppError = { kind: 'network' | 'unauthorized' | 'validation' | 'unknown'; message: string; status?: number;};export function mapHttpError(err: any): AppError { if (!err?.response) return { kind: 'network', message: 'Sem conexão. Tente novamente.' }; const status = err.response.status; if (status === 401) return { kind: 'unauthorized', message: 'Sessão expirada. Faça login novamente.', status }; if (status === 422) return { kind: 'validation', message: 'Verifique os campos e tente novamente.', status }; return { kind: 'unknown', message: 'Ocorreu um erro. Tente novamente.', status };}Boa prática: quando receber 401, dispare logout global e limpe token (evita loops de erro).
Etapa 5 — Autenticação: serviço + store + persistência
Separe:
- auth.service: chama API de login.
- auth.store: mantém estado (user, token, status) e expõe ações (signIn, signOut, bootstrap).
- secure storage: salva token com segurança.
// src/features/auth/auth.service.tsimport { http, mapHttpError } from '../../services/http/client';export async function login(email: string, password: string) { try { const { data } = await http.post('/auth/login', { email, password }); return data as { token: string; user: { id: string; name: string; email: string } }; } catch (e) { throw mapHttpError(e); }}O bootstrap do auth deve: ler token do storage, validar minimamente (ex.: existe), e setar signedIn ou signedOut.
Etapa 6 — Tarefas: serviços, cache local e estado global
Para tarefas, combine:
- tasks.service: funções
fetchTasks,fetchTaskById,createTask,updateTask. - tasks.cache: salvar/ler lista e detalhes no AsyncStorage (ou storage equivalente).
- tasks.store: estado global (lista, loading, error) e ações que orquestram API + cache.
Cache com estratégia “network-first com fallback”
Fluxo recomendado para lista:
- Tenta buscar da rede.
- Se sucesso: atualiza store e salva no cache.
- Se falha: tenta ler cache; se existir, usa cache e exibe aviso “modo offline”.
// src/features/tasks/tasks.store.ts (pseudo)async function loadTasks() { set({ loading: true, error: null }); try { const tasks = await tasksService.fetchTasks(); set({ tasks, loading: false }); await tasksCache.saveTasks(tasks); } catch (e) { const cached = await tasksCache.getTasks(); if (cached) { set({ tasks: cached, loading: false, error: { kind: 'network', message: 'Exibindo dados salvos (offline).' } }); } else { set({ loading: false, error: e }); } }}Ponto de atenção: cache não é “fonte da verdade” quando há escrita. Para escrita offline, implemente fila de ações (opcional) e sincronize quando voltar a conexão.
Etapa 7 — UI das telas (comportamento e estados)
Tela de Login
- Campos: email, senha.
- Validação: formato de email, senha mínima.
- Estados: loading no botão, erro global (ex.: credenciais inválidas).
- Ação:
auth.signInchama service, salva token, navega automaticamente ao mudar estado.
Lista de Tarefas (Tab “Tarefas”)
- Carrega ao focar a tela (ou no mount) e permite pull-to-refresh.
- Renderiza
FlatListcom item memoizado. - Estados: skeleton/loading inicial, empty state, erro com botão “tentar novamente”.
Detalhe da Tarefa (Stack)
- Mostra dados completos.
- Ações: mudar status (open → in_progress → done), anexos (foto/localização), editar descrição.
- Tratamento: otimista com rollback em erro ou pessimista (aguarda API).
Criar/Editar Tarefa (Tab “Criar” ou tela no Stack)
- Form com validação por campo.
- Botão “Salvar” desabilitado quando inválido ou enviando.
- Após sucesso: limpar form, atualizar lista (refetch ou update local), feedback visual.
Etapa 8 — Recurso do dispositivo: câmera OU localização
Escolha um recurso para o projeto final (ou implemente ambos se quiser). O importante é ter: solicitação de permissão, tratamento de negação e integração com o fluxo do formulário.
Opção A: Câmera (anexar foto)
- No formulário, botão “Adicionar foto”.
- Ao tocar: pedir permissão; se negada, mostrar instrução para habilitar nas configurações.
- Após capturar: mostrar preview e permitir remover.
- No envio: mandar
photoBase64ou multipart (conforme API).
// pseudo fluxo no handlerasync function handleAddPhoto() { const granted = await permissions.requestCamera(); if (!granted) { setError('Permissão de câmera negada. Habilite nas configurações.'); return; } const photo = await camera.takePhoto({ quality: 0.7, base64: true }); setValue('photoBase64', photo.base64);}Ponto de atenção: base64 aumenta payload e memória. Prefira upload multipart com arquivo quando possível; se usar base64, reduza qualidade e limite tamanho.
Opção B: Localização (registrar coordenadas)
- No formulário, botão “Usar minha localização”.
- Solicitar permissão de localização.
- Capturar coordenadas e exibir no UI (lat/lng) com opção de limpar.
// pseudo fluxo no handlerasync function handleGetLocation() { const granted = await permissions.requestLocation(); if (!granted) { setError('Permissão de localização negada.'); return; } const coords = await location.getCurrentPosition({ timeout: 10000 }); setValue('location', { lat: coords.latitude, lng: coords.longitude });}Ponto de atenção: trate timeout e indisponibilidade de GPS. Não bloqueie o formulário se a localização falhar; permita salvar sem localização.
Etapa 9 — Tratamento de erros e UX resiliente
Padrões recomendados
- Erros por camada: service lança
AppError; store decide fallback/cache; UI só renderiza mensagens. - Mensagens acionáveis: “Sem conexão” + botão “Tentar novamente”.
- Estados vazios: lista sem tarefas com CTA “Criar tarefa”.
- Timeouts: configure no HTTP e mostre feedback quando exceder.
- 401 global: logout automático e redirecionamento para login.
Exemplo de componente de erro reutilizável
// src/components/InlineError.tsximport React from 'react';import { View, Text, Button } from 'react-native';export function InlineError({ message, onRetry }: { message: string; onRetry?: () => void }) { return ( <View> <Text>{message}</Text> {onRetry ? <Button title="Tentar novamente" onPress={onRetry} /> : null} </View> );}Etapa 10 — Pontos de atenção de performance (no projeto final)
- Listas: use
FlatListcomkeyExtractor,initialNumToRendere itens memoizados; evite funções inline pesadas no render. - Imagens: redimensione antes de enviar; use cache quando aplicável; evite base64 para exibir imagens grandes.
- Re-renders: selecione apenas o necessário do estado global; evite passar objetos novos como props sem necessidade.
- Navegação: evite recriar navegadores a cada render; mantenha opções estáveis.
- Persistência: não grave no storage a cada tecla; grave em eventos (submit) ou com debounce.
Etapa 11 — Checklist final de build (Android e iOS)
Use esta lista para validar antes de gerar builds de teste/produção.
Configuração e ambiente
- Variáveis de ambiente:
API_URLcorreta para staging/produção; não hardcode em tela/service. - Chaves e segredos: token e dados sensíveis apenas em storage seguro; nunca em AsyncStorage.
- Permissões: câmera/localização declaradas e justificadas (strings de permissão no iOS e manifest no Android).
Qualidade e estabilidade
- Fluxo de login: token expira? validar logout automático em 401.
- Offline: sem rede, lista carrega do cache e ações críticas exibem erro claro.
- Erros de API: 4xx/5xx exibem mensagens amigáveis; sem crashes.
- Loading states: botões desabilitados durante envio; evita duplo submit.
Android
- Build type: debug vs release; testar release em dispositivo real.
- Permissões em runtime: fluxo de negar/permitir funciona.
- Back button: comportamento coerente (especialmente em stacks).
iOS
- Info.plist: descrições de uso de câmera/localização presentes.
- Permissões: testar primeira solicitação e após negar (orientar usuário a Settings).
- Safe areas: telas principais respeitam notch e barras.
Plano de execução (passo a passo resumido)
| Etapa | Entrega | Validação rápida |
|---|---|---|
| 1. Requisitos + tipos | Modelos e endpoints definidos | Tipos usados nas telas/services |
| 2. Bootstrap | Providers + auth bootstrap | App abre sem “piscar” login |
| 3. Navegação | AuthStack + Tabs + detalhes | Rotas privadas protegidas |
| 4. HTTP | Client + interceptors + AppError | 401 força logout |
| 5. Auth | Login/logout + token persistido | Reabrir app mantém sessão |
| 6. Tasks | Listar/detalhar/criar/atualizar | CRUD básico funcionando |
| 7. Cache | Network-first + fallback | Sem rede mostra cache |
| 8. Dispositivo | Câmera ou localização integrada | Permissões e fallback ok |
| 9. Polimento | Erros, loading, empty states | Sem crashes em fluxos comuns |
| 10. Build | Release testável | Android/iOS geram e rodam |