Ir para o conteúdo

Mobile - Diretrizes

Boas Práticas Obrigatórias

1. Autenticação

Armazenamento Seguro de Tokens

Sempre usar Expo Secure Store:

import * as SecureStore from 'expo-secure-store'

// Salvar
await SecureStore.setItemAsync('token', token)

// Ler
const token = await SecureStore.getItemAsync('token')

// Remover
await SecureStore.deleteItemAsync('token')

Nunca usar: - ❌ AsyncStorage para tokens - ❌ Variáveis de estado global - ❌ Props drilling

Interceptor de Token

Configurar no cliente Axios:

api.interceptors.request.use(async (config) => {
  const token = await SecureStore.getItemAsync('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

2. Tratamento de Erros

Erros de Rede

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    // Erro de rede (sem resposta)
    if (!error.response) {
      const networkError = new Error(
        'Não foi possível conectar ao servidor. Verifique sua conexão.'
      )
      return Promise.reject(networkError)
    }

    // 401: Token inválido/expirado
    if (error.response?.status === 401) {
      await SecureStore.deleteItemAsync('token')
      await SecureStore.deleteItemAsync('refresh_token')
      // Navegar para login
      navigation.navigate('Login')
    }

    return Promise.reject(error)
  }
)

Mensagens de Erro Amigáveis

const getErrorMessage = (error: any): string => {
  if (!error.response) {
    return 'Erro de conexão. Verifique sua internet.'
  }

  switch (error.response.status) {
    case 400:
      return error.response.data?.message || 'Dados inválidos'
    case 401:
      return 'Sessão expirada. Faça login novamente.'
    case 403:
      return 'Você não tem permissão para esta ação.'
    case 404:
      return 'Recurso não encontrado.'
    case 500:
      return 'Erro no servidor. Tente novamente mais tarde.'
    default:
      return 'Ocorreu um erro. Tente novamente.'
  }
}

3. Timeouts e Retry

Configuração do Axios

const api = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000, // 10 segundos
  headers: {
    'Content-Type': 'application/json',
  },
})

Configuração do React Query

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 2, // Tentar 2 vezes em caso de erro
      retryDelay: 1000, // 1 segundo entre tentativas
      staleTime: 5 * 60 * 1000, // 5 minutos
      cacheTime: 10 * 60 * 1000, // 10 minutos
    },
  },
})

4. Estratégia Offline

Detecção de Conectividade

import NetInfo from '@react-native-community/netinfo'

const checkConnection = async () => {
  const state = await NetInfo.fetch()
  return state.isConnected
}

Cache Local

Para dados críticos:

// Salvar dados localmente quando online
const saveLocalData = async (key: string, data: any) => {
  await AsyncStorage.setItem(key, JSON.stringify(data))
}

// Carregar dados locais quando offline
const loadLocalData = async (key: string) => {
  const data = await AsyncStorage.getItem(key)
  return data ? JSON.parse(data) : null
}

Sincronização ao Voltar Online

useEffect(() => {
  const unsubscribe = NetInfo.addEventListener(state => {
    if (state.isConnected) {
      // Sincronizar dados pendentes
      syncPendingData()
    }
  })

  return () => unsubscribe()
}, [])

5. Performance

Otimização de Listas

Sempre usar FlatList para listas grandes:

<FlatList
  data={items}
  renderItem={({ item }) => <Item data={item} />}
  keyExtractor={(item) => item.id.toString()}
  initialNumToRender={10}
  maxToRenderPerBatch={10}
  windowSize={10}
/>

Cache de Imagens

import { Image } from 'expo-image'

<Image
  source={{ uri: imageUrl }}
  cachePolicy="memory-disk"
  placeholder={placeholder}
/>

Lazy Loading

// Carregar dados sob demanda
const { data } = useQuery({
  queryKey: ['observations', page],
  queryFn: () => api.get('/observations', {
    params: { page, limit: 20 }
  }).then(res => res.data),
  enabled: !!page, // Só carregar quando page estiver definido
})

6. Validação de Formulários

Usar Zod + React Hook Form

import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'

const schema = z.object({
  email: z.string().email('Email inválido'),
  password: z.string().min(8, 'Senha deve ter no mínimo 8 caracteres'),
})

const { control, handleSubmit } = useForm({
  resolver: zodResolver(schema),
})

7. Navegação

Tipos de Navegação

// Definir tipos de navegação
export type RootStackParamList = {
  Login: undefined
  Register: undefined
  Dashboard: undefined
  Cycles: undefined
  CycleDetails: { cycleId: number }
}

// Usar tipos na navegação
navigation.navigate('CycleDetails', { cycleId: 1 })

Deep Linking

import * as Linking from 'expo-linking'

// Configurar deep links no app.json
// Usar Linking para abrir links externos
const handleDeepLink = (url: string) => {
  const { path, queryParams } = Linking.parse(url)
  // Navegar baseado no path
}

Versionamento e Compatibilidade

Versionamento da API

  • Mobile deve ser compatível com a versão atual da API
  • Mudanças breaking na API requerem atualização do mobile
  • Manter compatibilidade retroativa quando possível

Versionamento do App

  • Usar versionamento semântico (ex: 1.0.0)
  • Incrementar versão em cada release
  • Documentar mudanças em changelog

Testes

Testes Unitários

import { render, fireEvent } from '@testing-library/react-native'

test('should handle login', () => {
  const { getByPlaceholderText, getByText } = render(<Login />)
  const emailInput = getByPlaceholderText('Email')
  const passwordInput = getByPlaceholderText('Senha')

  fireEvent.changeText(emailInput, 'user@example.com')
  fireEvent.changeText(passwordInput, 'password123')
  fireEvent.press(getByText('Entrar'))

  // Verificar comportamento esperado
})

Testes de Integração

  • Testar fluxos completos (login → dashboard → registro)
  • Testar sincronização
  • Testar tratamento de erros

Acessibilidade

Labels e Hints

<TextInput
  accessibilityLabel="Email"
  accessibilityHint="Digite seu endereço de email"
  placeholder="Email"
/>

Contraste e Tamanhos

  • Manter contraste adequado (WCAG AA)
  • Tamanhos de fonte legíveis (mínimo 14px)
  • Áreas de toque adequadas (mínimo 44x44 pontos)

Segurança

Nunca Expor

  • ❌ Tokens em logs
  • ❌ Credenciais em código
  • ❌ Secrets em variáveis de ambiente públicas

Sempre Validar

  • ✅ Dados de entrada do usuário
  • ✅ Respostas da API
  • ✅ Permissões antes de acessar recursos

Referências