Projeto final em React Native Essencial: app completo integrando navegação, estado, API e recursos do dispositivo

Capítulo 16

Tempo estimado de leitura: 11 minutos

+ Exercício

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:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 → retorna token e user
  • GET /tasks → lista tarefas
  • GET /tasks/:id → detalhes
  • POST /tasks → cria tarefa
  • PATCH /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.signIn chama 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 FlatList com 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 photoBase64 ou 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 FlatList com keyExtractor, initialNumToRender e 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_URL correta 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)

EtapaEntregaValidação rápida
1. Requisitos + tiposModelos e endpoints definidosTipos usados nas telas/services
2. BootstrapProviders + auth bootstrapApp abre sem “piscar” login
3. NavegaçãoAuthStack + Tabs + detalhesRotas privadas protegidas
4. HTTPClient + interceptors + AppError401 força logout
5. AuthLogin/logout + token persistidoReabrir app mantém sessão
6. TasksListar/detalhar/criar/atualizarCRUD básico funcionando
7. CacheNetwork-first + fallbackSem rede mostra cache
8. DispositivoCâmera ou localização integradaPermissões e fallback ok
9. PolimentoErros, loading, empty statesSem crashes em fluxos comuns
10. BuildRelease testávelAndroid/iOS geram e rodam

Agora responda o exercício sobre o conteúdo:

Ao implementar cache para a lista de tarefas em um app com suporte parcial a offline, qual estratégia garante que o app tente usar dados atualizados, mas ainda funcione sem rede quando possível?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A estratégia network-first com fallback tenta obter dados atuais pela rede e, se falhar, usa o cache quando disponível, mantendo a UI funcional e informando o usuário sobre o modo offline.

Capa do Ebook gratuito React Native Essencial: criando apps completos com JavaScript e boas práticas
100%

React Native Essencial: criando apps completos com JavaScript e boas práticas

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.