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