@the-bearded-bear/claude-craft 8.8.1 → 8.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dev/i18n/de/Angular/commands/check-compliance.md +218 -168
- package/Dev/i18n/de/Common/agents/refactoring-specialist.md +146 -69
- package/Dev/i18n/de/Common/commands/add-technology.md +166 -68
- package/Dev/i18n/de/Common/commands/setup-ci.md +216 -33
- package/Dev/i18n/de/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/de/Python/checklists/new-feature.md +319 -44
- package/Dev/i18n/de/React/rules/02-architecture.md +483 -322
- package/Dev/i18n/de/React/rules/03-coding-standards.md +584 -368
- package/Dev/i18n/de/Symfony/commands/check-compliance.md +186 -114
- package/Dev/i18n/de/Symfony/rules/07-testing-symfony.md +679 -10
- package/Dev/i18n/de/UIUX/commands/a11y-component.md +284 -26
- package/Dev/i18n/de/UIUX/commands/design-tokens.md +199 -35
- package/Dev/i18n/de/UIUX/commands/user-flow.md +179 -16
- package/Dev/i18n/de/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/es/Angular/commands/check-compliance.md +217 -167
- package/Dev/i18n/es/Common/commands/add-technology.md +158 -60
- package/Dev/i18n/es/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/es/React/commands/check-testing.md +263 -169
- package/Dev/i18n/es/ReactNative/commands/check-testing.md +256 -198
- package/Dev/i18n/es/Symfony/commands/check-compliance.md +189 -117
- package/Dev/i18n/es/Symfony/commands/check-testing.md +301 -102
- package/Dev/i18n/es/Symfony/commands/migration-plan.md +267 -34
- package/Dev/i18n/es/UIUX/commands/a11y-component.md +274 -16
- package/Dev/i18n/es/UIUX/commands/design-tokens.md +186 -22
- package/Dev/i18n/es/UIUX/commands/user-flow.md +173 -10
- package/Dev/i18n/es/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/fr/Angular/commands/check-compliance.md +217 -167
- package/Dev/i18n/fr/Common/commands/ralph-run.md +138 -60
- package/Dev/i18n/fr/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/fr/React/commands/check-architecture.md +370 -53
- package/Dev/i18n/fr/React/commands/check-code-quality.md +454 -64
- package/Dev/i18n/fr/React/commands/check-testing.md +476 -102
- package/Dev/i18n/fr/Symfony/commands/check-compliance.md +188 -116
- package/Dev/i18n/fr/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/pt/Angular/commands/check-compliance.md +225 -175
- package/Dev/i18n/pt/Common/agents/ralph-conductor.md +211 -30
- package/Dev/i18n/pt/Common/commands/add-technology.md +149 -51
- package/Dev/i18n/pt/Common/commands/ralph-run.md +239 -35
- package/Dev/i18n/pt/Flutter/commands/analyze-performance.md +331 -46
- package/Dev/i18n/pt/Flutter/commands/check-compliance.md +234 -29
- package/Dev/i18n/pt/Flutter/commands/generate-feature.md +526 -51
- package/Dev/i18n/pt/Flutter/commands/generate-widget.md +515 -27
- package/Dev/i18n/pt/Flutter/commands/golden-update.md +322 -46
- package/Dev/i18n/pt/Flutter/commands/localization-check.md +278 -37
- package/Dev/i18n/pt/Laravel/commands/check-compliance.md +212 -104
- package/Dev/i18n/pt/React/commands/accessibility-check.md +151 -42
- package/Dev/i18n/pt/React/commands/bundle-analyze.md +300 -25
- package/Dev/i18n/pt/React/commands/check-architecture.md +369 -34
- package/Dev/i18n/pt/React/commands/check-code-quality.md +440 -34
- package/Dev/i18n/pt/React/commands/check-compliance.md +232 -62
- package/Dev/i18n/pt/React/commands/check-security.md +370 -31
- package/Dev/i18n/pt/React/commands/check-testing.md +473 -43
- package/Dev/i18n/pt/React/commands/generate-component.md +454 -19
- package/Dev/i18n/pt/React/commands/generate-hook.md +425 -24
- package/Dev/i18n/pt/React/commands/storybook-story.md +520 -35
- package/Dev/i18n/pt/ReactNative/commands/app-size.md +358 -387
- package/Dev/i18n/pt/ReactNative/commands/check-code-quality.md +246 -75
- package/Dev/i18n/pt/ReactNative/commands/check-compliance.md +191 -85
- package/Dev/i18n/pt/ReactNative/commands/check-security.md +363 -115
- package/Dev/i18n/pt/ReactNative/commands/check-testing.md +269 -79
- package/Dev/i18n/pt/ReactNative/commands/generate-screen.md +491 -324
- package/Dev/i18n/pt/ReactNative/commands/native-module.md +539 -82
- package/Dev/i18n/pt/ReactNative/commands/store-prepare.md +429 -185
- package/Dev/i18n/pt/ReactNative/rules/02-architecture.md +1084 -31
- package/Dev/i18n/pt/Symfony/commands/check-compliance.md +195 -123
- package/Dev/i18n/pt/UIUX/commands/a11y-audit.md +208 -24
- package/Dev/i18n/pt/UIUX/commands/a11y-component.md +281 -23
- package/Dev/i18n/pt/UIUX/commands/design-tokens.md +197 -33
- package/Dev/i18n/pt/UIUX/commands/user-flow.md +183 -20
- package/Dev/i18n/pt/VueJS/commands/check-compliance.md +228 -103
- package/Dev/i18n/pt/Workflow/commands/analyze.md +147 -146
- package/Dev/i18n/pt/Workflow/commands/design.md +205 -204
- package/Dev/i18n/pt/Workflow/commands/implement.md +184 -184
- package/Project/i18n/de/Sprint/commands/dev.md +339 -86
- package/Project/i18n/es/Sprint/commands/dev.md +342 -91
- package/Project/i18n/pt/Sprint/commands/dev.md +343 -92
- package/bundles/cursor/.cursorrules +1 -1
- package/bundles/windsurf/.windsurfrules +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Introdução
|
|
4
4
|
|
|
5
|
-
Este documento define a arquitetura recomendada para aplicações React Native com TypeScript e Expo, baseada nas melhores práticas
|
|
5
|
+
Este documento define a arquitetura recomendada para aplicações React Native com TypeScript e Expo, baseada nas melhores práticas do setor.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Este documento define a arquitetura recomendada para aplicações React Native c
|
|
|
10
10
|
|
|
11
11
|
### 1. Clean Architecture
|
|
12
12
|
|
|
13
|
-
A aplicação é organizada em **camadas** com responsabilidades
|
|
13
|
+
A aplicação é organizada em **camadas** com responsabilidades bem definidas:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
┌─────────────────────────────────────┐
|
|
@@ -20,21 +20,21 @@ A aplicação é organizada em **camadas** com responsabilidades claras:
|
|
|
20
20
|
├─────────────────────────────────────┤
|
|
21
21
|
│ CAMADA DE DOMÍNIO │ <- Lógica de Negócio, Tipos
|
|
22
22
|
├─────────────────────────────────────┤
|
|
23
|
-
│ CAMADA DE DADOS │ <- API,
|
|
23
|
+
│ CAMADA DE DADOS │ <- API, Armazenamento, Serviços
|
|
24
24
|
└─────────────────────────────────────┘
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
**Regras de dependência**:
|
|
28
28
|
- Camadas superiores podem depender de camadas inferiores
|
|
29
29
|
- Camadas inferiores NÃO DEVEM depender de camadas superiores
|
|
30
|
-
- Cada camada tem uma
|
|
30
|
+
- Cada camada tem uma única responsabilidade bem definida
|
|
31
31
|
|
|
32
|
-
### 2. Organização Baseada em
|
|
32
|
+
### 2. Organização Baseada em Funcionalidades
|
|
33
33
|
|
|
34
|
-
Organização por **
|
|
34
|
+
Organização por **funcionalidade de negócio** em vez de tipo técnico:
|
|
35
35
|
|
|
36
36
|
```typescript
|
|
37
|
-
// ✅ BOM: Baseado em
|
|
37
|
+
// ✅ BOM: Baseado em funcionalidade
|
|
38
38
|
features/
|
|
39
39
|
├── auth/
|
|
40
40
|
│ ├── screens/
|
|
@@ -56,7 +56,7 @@ types/
|
|
|
56
56
|
|
|
57
57
|
### 3. Separação de Responsabilidades
|
|
58
58
|
|
|
59
|
-
Cada arquivo, função
|
|
59
|
+
Cada arquivo, função e componente tem **UMA responsabilidade**:
|
|
60
60
|
|
|
61
61
|
```typescript
|
|
62
62
|
// ✅ BOM: Separação clara
|
|
@@ -77,7 +77,7 @@ export const LoginButton = () => {
|
|
|
77
77
|
const handleLogin = async () => {
|
|
78
78
|
setLoading(true);
|
|
79
79
|
const response = await fetch('/api/login');
|
|
80
|
-
// Lógica + UI
|
|
80
|
+
// Lógica + UI misturadas
|
|
81
81
|
};
|
|
82
82
|
return <Pressable onPress={handleLogin}>Login</Pressable>;
|
|
83
83
|
};
|
|
@@ -87,21 +87,21 @@ export const LoginButton = () => {
|
|
|
87
87
|
|
|
88
88
|
## Estrutura de Pastas
|
|
89
89
|
|
|
90
|
-
### Visão Completa
|
|
90
|
+
### Visão Geral Completa
|
|
91
91
|
|
|
92
92
|
```
|
|
93
93
|
my-app/
|
|
94
94
|
├── src/
|
|
95
95
|
│ ├── app/ # Expo Router (App Router)
|
|
96
|
-
│ │ ├── (auth)/ # Grupo
|
|
96
|
+
│ │ ├── (auth)/ # Grupo auth (layout compartilhado)
|
|
97
97
|
│ │ │ ├── login.tsx
|
|
98
98
|
│ │ │ ├── register.tsx
|
|
99
99
|
│ │ │ └── _layout.tsx
|
|
100
|
-
│ │ ├── (tabs)/ # Grupo
|
|
101
|
-
│ │ │ ├── index.tsx #
|
|
102
|
-
│ │ │ ├── profile.tsx #
|
|
103
|
-
│ │ │ ├── settings.tsx #
|
|
104
|
-
│ │ │ └── _layout.tsx # Layout
|
|
100
|
+
│ │ ├── (tabs)/ # Grupo tabs (abas de navegação)
|
|
101
|
+
│ │ │ ├── index.tsx # Aba inicial
|
|
102
|
+
│ │ │ ├── profile.tsx # Aba de perfil
|
|
103
|
+
│ │ │ ├── settings.tsx # Aba de configurações
|
|
104
|
+
│ │ │ └── _layout.tsx # Layout das abas
|
|
105
105
|
│ │ ├── article/
|
|
106
106
|
│ │ │ └── [id].tsx # Rota dinâmica
|
|
107
107
|
│ │ ├── modal.tsx # Tela modal
|
|
@@ -110,35 +110,145 @@ my-app/
|
|
|
110
110
|
│ │
|
|
111
111
|
│ ├── components/ # Componentes reutilizáveis
|
|
112
112
|
│ │ ├── ui/ # Componentes UI base
|
|
113
|
+
│ │ │ ├── Button/
|
|
114
|
+
│ │ │ │ ├── Button.tsx
|
|
115
|
+
│ │ │ │ ├── Button.styles.ts
|
|
116
|
+
│ │ │ │ ├── Button.test.tsx
|
|
117
|
+
│ │ │ │ └── index.ts
|
|
118
|
+
│ │ │ ├── Input/
|
|
119
|
+
│ │ │ ├── Card/
|
|
120
|
+
│ │ │ └── index.ts
|
|
113
121
|
│ │ ├── forms/ # Componentes de formulário
|
|
122
|
+
│ │ │ ├── LoginForm/
|
|
123
|
+
│ │ │ ├── ProfileForm/
|
|
124
|
+
│ │ │ └── index.ts
|
|
114
125
|
│ │ ├── layout/ # Componentes de layout
|
|
126
|
+
│ │ │ ├── Container/
|
|
127
|
+
│ │ │ ├── SafeArea/
|
|
128
|
+
│ │ │ └── index.ts
|
|
115
129
|
│ │ └── shared/ # Componentes compartilhados
|
|
130
|
+
│ │ ├── Header/
|
|
131
|
+
│ │ ├── Footer/
|
|
132
|
+
│ │ └── index.ts
|
|
116
133
|
│ │
|
|
117
|
-
│ ├── features/ #
|
|
134
|
+
│ ├── features/ # Funcionalidades por domínio de negócio
|
|
118
135
|
│ │ ├── auth/
|
|
119
136
|
│ │ │ ├── components/ # Componentes específicos de auth
|
|
137
|
+
│ │ │ │ ├── SocialLoginButtons/
|
|
138
|
+
│ │ │ │ └── PasswordStrength/
|
|
120
139
|
│ │ │ ├── hooks/ # Hooks de auth
|
|
140
|
+
│ │ │ │ ├── useAuth.ts
|
|
141
|
+
│ │ │ │ ├── useLogin.ts
|
|
142
|
+
│ │ │ │ └── useRegister.ts
|
|
121
143
|
│ │ │ ├── services/ # Serviços de auth
|
|
122
|
-
│ │ │ ├──
|
|
123
|
-
│ │ │
|
|
124
|
-
│ │ │
|
|
144
|
+
│ │ │ │ ├── auth.service.ts
|
|
145
|
+
│ │ │ │ └── token.service.ts
|
|
146
|
+
│ │ │ ├── stores/ # Gerenciamento de estado de auth
|
|
147
|
+
│ │ │ │ └── auth.store.ts
|
|
148
|
+
│ │ │ ├── types/ # Tipos de auth
|
|
149
|
+
│ │ │ │ └── Auth.types.ts
|
|
150
|
+
│ │ │ └── utils/ # Utilitários de auth
|
|
151
|
+
│ │ │ └── validation.ts
|
|
125
152
|
│ │ ├── profile/
|
|
153
|
+
│ │ │ ├── components/
|
|
154
|
+
│ │ │ ├── hooks/
|
|
155
|
+
│ │ │ ├── services/
|
|
156
|
+
│ │ │ └── types/
|
|
126
157
|
│ │ └── articles/
|
|
158
|
+
│ │ ├── components/
|
|
159
|
+
│ │ ├── hooks/
|
|
160
|
+
│ │ ├── services/
|
|
161
|
+
│ │ └── types/
|
|
127
162
|
│ │
|
|
128
163
|
│ ├── hooks/ # Hooks globais/compartilhados
|
|
164
|
+
│ │ ├── useAppState.ts # Estado do app (foreground/background)
|
|
165
|
+
│ │ ├── useKeyboard.ts # Estado do teclado
|
|
166
|
+
│ │ ├── useOrientation.ts # Orientação do dispositivo
|
|
167
|
+
│ │ ├── useNetworkStatus.ts # Conectividade de rede
|
|
168
|
+
│ │ └── index.ts
|
|
169
|
+
│ │
|
|
129
170
|
│ ├── services/ # Serviços globais
|
|
171
|
+
│ │ ├── api/ # Serviços de API
|
|
172
|
+
│ │ │ ├── client.ts # Cliente da API (Axios)
|
|
173
|
+
│ │ │ ├── interceptors.ts # Interceptadores de requisição/resposta
|
|
174
|
+
│ │ │ ├── endpoints.ts # Constantes de endpoints da API
|
|
175
|
+
│ │ │ └── index.ts
|
|
176
|
+
│ │ ├── storage/ # Serviços de armazenamento
|
|
177
|
+
│ │ │ ├── mmkv.storage.ts # Armazenamento MMKV
|
|
178
|
+
│ │ │ ├── secure.storage.ts # Armazenamento seguro
|
|
179
|
+
│ │ │ └── index.ts
|
|
180
|
+
│ │ ├── analytics/ # Serviço de analytics
|
|
181
|
+
│ │ │ ├── analytics.service.ts
|
|
182
|
+
│ │ │ └── events.ts
|
|
183
|
+
│ │ ├── notifications/ # Serviço de notificações
|
|
184
|
+
│ │ │ └── push.service.ts
|
|
185
|
+
│ │ └── location/ # Serviço de localização
|
|
186
|
+
│ │ └── location.service.ts
|
|
187
|
+
│ │
|
|
130
188
|
│ ├── stores/ # Gerenciamento de estado global
|
|
189
|
+
│ │ ├── app.store.ts # Estado global do app
|
|
190
|
+
│ │ ├── theme.store.ts # Estado do tema
|
|
191
|
+
│ │ └── index.ts
|
|
192
|
+
│ │
|
|
193
|
+
│ ├── navigation/ # Configuração de navegação
|
|
194
|
+
│ │ ├── types.ts # Tipos de navegação
|
|
195
|
+
│ │ ├── linking.ts # Configuração de deep linking
|
|
196
|
+
│ │ └── index.ts
|
|
197
|
+
│ │
|
|
131
198
|
│ ├── utils/ # Utilitários
|
|
199
|
+
│ │ ├── date.utils.ts
|
|
200
|
+
│ │ ├── string.utils.ts
|
|
201
|
+
│ │ ├── number.utils.ts
|
|
202
|
+
│ │ ├── validation.utils.ts
|
|
203
|
+
│ │ └── index.ts
|
|
204
|
+
│ │
|
|
132
205
|
│ ├── constants/ # Constantes
|
|
206
|
+
│ │ ├── app.constants.ts
|
|
207
|
+
│ │ ├── api.constants.ts
|
|
208
|
+
│ │ ├── storage.constants.ts
|
|
209
|
+
│ │ └── index.ts
|
|
210
|
+
│ │
|
|
133
211
|
│ ├── types/ # Tipos globais
|
|
212
|
+
│ │ ├── global.types.ts
|
|
213
|
+
│ │ ├── api.types.ts
|
|
214
|
+
│ │ ├── navigation.types.ts
|
|
215
|
+
│ │ └── index.ts
|
|
216
|
+
│ │
|
|
134
217
|
│ ├── config/ # Configuração
|
|
218
|
+
│ │ ├── env.ts # Variáveis de ambiente
|
|
219
|
+
│ │ ├── app.config.ts # Configuração do app
|
|
220
|
+
│ │ └── index.ts
|
|
221
|
+
│ │
|
|
135
222
|
│ ├── theme/ # Tema
|
|
223
|
+
│ │ ├── colors.ts
|
|
224
|
+
│ │ ├── spacing.ts
|
|
225
|
+
│ │ ├── typography.ts
|
|
226
|
+
│ │ ├── shadows.ts
|
|
227
|
+
│ │ └── index.ts
|
|
228
|
+
│ │
|
|
136
229
|
│ └── assets/ # Assets
|
|
230
|
+
│ ├── images/
|
|
231
|
+
│ ├── fonts/
|
|
232
|
+
│ ├── icons/
|
|
233
|
+
│ └── animations/
|
|
137
234
|
│
|
|
138
235
|
├── __tests__/ # Testes
|
|
139
236
|
│ ├── unit/
|
|
140
237
|
│ ├── integration/
|
|
141
238
|
│ └── e2e/
|
|
239
|
+
│
|
|
240
|
+
├── .expo/ # Gerado pelo Expo
|
|
241
|
+
├── .husky/ # Git hooks
|
|
242
|
+
├── node_modules/
|
|
243
|
+
├── app.json # Configuração do app Expo
|
|
244
|
+
├── babel.config.js
|
|
245
|
+
├── tsconfig.json
|
|
246
|
+
├── package.json
|
|
247
|
+
├── .eslintrc.js
|
|
248
|
+
├── .prettierrc.js
|
|
249
|
+
├── .env.development
|
|
250
|
+
├── .env.production
|
|
251
|
+
└── README.md
|
|
142
252
|
```
|
|
143
253
|
|
|
144
254
|
---
|
|
@@ -149,7 +259,7 @@ my-app/
|
|
|
149
259
|
|
|
150
260
|
#### A. App Router (Expo Router)
|
|
151
261
|
|
|
152
|
-
**Nova arquitetura de roteamento
|
|
262
|
+
**Nova arquitetura de roteamento baseado em arquivos**:
|
|
153
263
|
|
|
154
264
|
```typescript
|
|
155
265
|
// src/app/_layout.tsx - Layout raiz
|
|
@@ -167,34 +277,977 @@ export default function RootLayout() {
|
|
|
167
277
|
</QueryClientProvider>
|
|
168
278
|
);
|
|
169
279
|
}
|
|
280
|
+
|
|
281
|
+
// src/app/(tabs)/_layout.tsx - Layout das abas
|
|
282
|
+
import { Tabs } from 'expo-router';
|
|
283
|
+
|
|
284
|
+
export default function TabsLayout() {
|
|
285
|
+
return (
|
|
286
|
+
<Tabs>
|
|
287
|
+
<Tabs.Screen
|
|
288
|
+
name="index"
|
|
289
|
+
options={{
|
|
290
|
+
title: 'Home',
|
|
291
|
+
tabBarIcon: ({ color }) => <HomeIcon color={color} />,
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
<Tabs.Screen
|
|
295
|
+
name="profile"
|
|
296
|
+
options={{
|
|
297
|
+
title: 'Profile',
|
|
298
|
+
tabBarIcon: ({ color }) => <ProfileIcon color={color} />,
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
</Tabs>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/app/(tabs)/index.tsx - Tela inicial
|
|
306
|
+
export default function HomeScreen() {
|
|
307
|
+
return <HomeView />;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/app/article/[id].tsx - Rota dinâmica
|
|
311
|
+
import { useLocalSearchParams } from 'expo-router';
|
|
312
|
+
|
|
313
|
+
export default function ArticleScreen() {
|
|
314
|
+
const { id } = useLocalSearchParams();
|
|
315
|
+
return <ArticleDetail id={id} />;
|
|
316
|
+
}
|
|
170
317
|
```
|
|
171
318
|
|
|
172
319
|
**Vantagens do Expo Router**:
|
|
173
320
|
- Roteamento baseado em arquivos (como Next.js)
|
|
174
|
-
- Navegação
|
|
321
|
+
- Navegação com tipagem segura
|
|
175
322
|
- Deep linking automático
|
|
176
|
-
- SEO
|
|
323
|
+
- Amigável para SEO (Expo Web)
|
|
177
324
|
- Layouts compartilhados
|
|
178
325
|
- Navegação aninhada simplificada
|
|
179
326
|
|
|
327
|
+
#### B. Componentes
|
|
328
|
+
|
|
329
|
+
**Organização hierárquica**:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// components/ui/Button/Button.tsx
|
|
333
|
+
import { Pressable, Text } from 'react-native';
|
|
334
|
+
import { styles } from './Button.styles';
|
|
335
|
+
import type { ButtonProps } from './Button.types';
|
|
336
|
+
|
|
337
|
+
export const Button = ({
|
|
338
|
+
children,
|
|
339
|
+
variant = 'primary',
|
|
340
|
+
onPress,
|
|
341
|
+
disabled,
|
|
342
|
+
}: ButtonProps) => {
|
|
343
|
+
return (
|
|
344
|
+
<Pressable
|
|
345
|
+
style={[styles.button, styles[variant], disabled && styles.disabled]}
|
|
346
|
+
onPress={onPress}
|
|
347
|
+
disabled={disabled}
|
|
348
|
+
>
|
|
349
|
+
<Text style={styles.text}>{children}</Text>
|
|
350
|
+
</Pressable>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// components/ui/Button/Button.styles.ts
|
|
355
|
+
import { StyleSheet } from 'react-native';
|
|
356
|
+
import { theme } from '@/theme';
|
|
357
|
+
|
|
358
|
+
export const styles = StyleSheet.create({
|
|
359
|
+
button: {
|
|
360
|
+
paddingHorizontal: theme.spacing.md,
|
|
361
|
+
paddingVertical: theme.spacing.sm,
|
|
362
|
+
borderRadius: theme.borderRadius.md,
|
|
363
|
+
alignItems: 'center',
|
|
364
|
+
},
|
|
365
|
+
primary: {
|
|
366
|
+
backgroundColor: theme.colors.primary,
|
|
367
|
+
},
|
|
368
|
+
secondary: {
|
|
369
|
+
backgroundColor: theme.colors.secondary,
|
|
370
|
+
},
|
|
371
|
+
disabled: {
|
|
372
|
+
opacity: 0.5,
|
|
373
|
+
},
|
|
374
|
+
text: {
|
|
375
|
+
color: theme.colors.white,
|
|
376
|
+
fontSize: theme.typography.fontSize.md,
|
|
377
|
+
fontWeight: '600',
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// components/ui/Button/Button.types.ts
|
|
382
|
+
import type { PressableProps } from 'react-native';
|
|
383
|
+
|
|
384
|
+
export interface ButtonProps extends Omit<PressableProps, 'style'> {
|
|
385
|
+
children: React.ReactNode;
|
|
386
|
+
variant?: 'primary' | 'secondary' | 'outline';
|
|
387
|
+
disabled?: boolean;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// components/ui/Button/index.ts
|
|
391
|
+
export { Button } from './Button';
|
|
392
|
+
export type { ButtonProps } from './Button.types';
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Tipos de componentes**:
|
|
396
|
+
|
|
397
|
+
1. **Componentes UI** (burros/apresentacionais):
|
|
398
|
+
- Sem lógica de negócio
|
|
399
|
+
- Orientados por props
|
|
400
|
+
- Altamente reutilizáveis
|
|
401
|
+
- Facilmente testáveis
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// components/ui/Card/Card.tsx
|
|
405
|
+
interface CardProps {
|
|
406
|
+
children: React.ReactNode;
|
|
407
|
+
onPress?: () => void;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export const Card = ({ children, onPress }: CardProps) => (
|
|
411
|
+
<Pressable onPress={onPress} style={styles.card}>
|
|
412
|
+
{children}
|
|
413
|
+
</Pressable>
|
|
414
|
+
);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
2. **Componentes Inteligentes** (containers):
|
|
418
|
+
- Conectados ao estado
|
|
419
|
+
- Lidam com lógica de negócio
|
|
420
|
+
- Utilizam hooks
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// features/articles/components/ArticleList/ArticleList.tsx
|
|
424
|
+
export const ArticleList = () => {
|
|
425
|
+
const { data: articles, isLoading } = useArticles();
|
|
426
|
+
|
|
427
|
+
if (isLoading) return <LoadingSpinner />;
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<FlatList
|
|
431
|
+
data={articles}
|
|
432
|
+
renderItem={({ item }) => <ArticleCard article={item} />}
|
|
433
|
+
keyExtractor={(item) => item.id}
|
|
434
|
+
/>
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
3. **Componentes Compostos**:
|
|
440
|
+
- Componentes composicionais
|
|
441
|
+
- API intuitiva
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// components/ui/Form/Form.tsx
|
|
445
|
+
export const Form = ({ children, onSubmit }: FormProps) => (
|
|
446
|
+
<View style={styles.form}>{children}</View>
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
Form.Field = ({ label, children }: FieldProps) => (
|
|
450
|
+
<View style={styles.field}>
|
|
451
|
+
<Text style={styles.label}>{label}</Text>
|
|
452
|
+
{children}
|
|
453
|
+
</View>
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
Form.Submit = ({ children, ...props }: SubmitProps) => (
|
|
457
|
+
<Button {...props}>{children}</Button>
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Uso:
|
|
461
|
+
<Form onSubmit={handleSubmit}>
|
|
462
|
+
<Form.Field label="Email">
|
|
463
|
+
<Input name="email" />
|
|
464
|
+
</Form.Field>
|
|
465
|
+
<Form.Submit>Enviar</Form.Submit>
|
|
466
|
+
</Form>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### 2. Camada de Aplicação (Lógica)
|
|
472
|
+
|
|
473
|
+
#### A. Custom Hooks
|
|
474
|
+
|
|
475
|
+
**Encapsulam lógica reutilizável**:
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// hooks/useAuth.ts
|
|
479
|
+
import { useAuthStore } from '@/features/auth/stores/auth.store';
|
|
480
|
+
import { authService } from '@/features/auth/services/auth.service';
|
|
481
|
+
|
|
482
|
+
export const useAuth = () => {
|
|
483
|
+
const { user, setUser, clearUser } = useAuthStore();
|
|
484
|
+
|
|
485
|
+
const login = async (credentials: Credentials) => {
|
|
486
|
+
const response = await authService.login(credentials);
|
|
487
|
+
setUser(response.user);
|
|
488
|
+
return response;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const logout = async () => {
|
|
492
|
+
await authService.logout();
|
|
493
|
+
clearUser();
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const isAuthenticated = !!user;
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
user,
|
|
500
|
+
login,
|
|
501
|
+
logout,
|
|
502
|
+
isAuthenticated,
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Uso no componente:
|
|
507
|
+
const LoginScreen = () => {
|
|
508
|
+
const { login, isAuthenticated } = useAuth();
|
|
509
|
+
const { mutate, isLoading } = useMutation({
|
|
510
|
+
mutationFn: login,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (isAuthenticated) {
|
|
514
|
+
return <Redirect href="/(tabs)" />;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return <LoginForm onSubmit={mutate} isLoading={isLoading} />;
|
|
518
|
+
};
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Padrões comuns de hooks**:
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// 1. Hook de busca de dados
|
|
525
|
+
export const useArticles = (filters?: ArticleFilters) => {
|
|
526
|
+
return useQuery({
|
|
527
|
+
queryKey: ['articles', filters],
|
|
528
|
+
queryFn: () => articlesService.getAll(filters),
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// 2. Hook de mutação
|
|
533
|
+
export const useCreateArticle = () => {
|
|
534
|
+
const queryClient = useQueryClient();
|
|
535
|
+
|
|
536
|
+
return useMutation({
|
|
537
|
+
mutationFn: articlesService.create,
|
|
538
|
+
onSuccess: () => {
|
|
539
|
+
queryClient.invalidateQueries({ queryKey: ['articles'] });
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// 3. Hook específico de plataforma
|
|
545
|
+
export const usePlatform = () => {
|
|
546
|
+
const isIOS = Platform.OS === 'ios';
|
|
547
|
+
const isAndroid = Platform.OS === 'android';
|
|
548
|
+
const isWeb = Platform.OS === 'web';
|
|
549
|
+
|
|
550
|
+
return { isIOS, isAndroid, isWeb };
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// 4. Hook de informações do dispositivo
|
|
554
|
+
export const useDeviceInfo = () => {
|
|
555
|
+
const { width, height } = useWindowDimensions();
|
|
556
|
+
const isTablet = width > 768;
|
|
557
|
+
const isLandscape = width > height;
|
|
558
|
+
|
|
559
|
+
return { width, height, isTablet, isLandscape };
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// 5. Hook de teclado
|
|
563
|
+
export const useKeyboard = () => {
|
|
564
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
565
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
566
|
+
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
const showListener = Keyboard.addListener('keyboardDidShow', (e) => {
|
|
569
|
+
setIsVisible(true);
|
|
570
|
+
setKeyboardHeight(e.endCoordinates.height);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const hideListener = Keyboard.addListener('keyboardDidHide', () => {
|
|
574
|
+
setIsVisible(false);
|
|
575
|
+
setKeyboardHeight(0);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return () => {
|
|
579
|
+
showListener.remove();
|
|
580
|
+
hideListener.remove();
|
|
581
|
+
};
|
|
582
|
+
}, []);
|
|
583
|
+
|
|
584
|
+
return { isVisible, keyboardHeight };
|
|
585
|
+
};
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
#### B. Gerenciamento de Estado
|
|
589
|
+
|
|
590
|
+
**Arquitetura em múltiplos níveis**:
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
// 1. Estado Local (useState, useReducer)
|
|
594
|
+
// Para estado isolado em um componente
|
|
595
|
+
const [count, setCount] = useState(0);
|
|
596
|
+
|
|
597
|
+
// 2. Estado de URL (Expo Router)
|
|
598
|
+
// Para estado sincronizado com a URL
|
|
599
|
+
const { id, filter } = useLocalSearchParams<{ id: string; filter: string }>();
|
|
600
|
+
|
|
601
|
+
// 3. Estado do Servidor (React Query)
|
|
602
|
+
// Para dados do servidor com cache
|
|
603
|
+
const { data } = useQuery({
|
|
604
|
+
queryKey: ['user', userId],
|
|
605
|
+
queryFn: () => fetchUser(userId),
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// 4. Estado Global (Zustand)
|
|
609
|
+
// Para estado global do lado do cliente
|
|
610
|
+
// stores/theme.store.ts
|
|
611
|
+
import { create } from 'zustand';
|
|
612
|
+
import { persist } from 'zustand/middleware';
|
|
613
|
+
import { createMMKVStorage } from '@/services/storage/mmkv.storage';
|
|
614
|
+
|
|
615
|
+
interface ThemeState {
|
|
616
|
+
isDark: boolean;
|
|
617
|
+
toggleTheme: () => void;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export const useThemeStore = create<ThemeState>()(
|
|
621
|
+
persist(
|
|
622
|
+
(set) => ({
|
|
623
|
+
isDark: false,
|
|
624
|
+
toggleTheme: () => set((state) => ({ isDark: !state.isDark })),
|
|
625
|
+
}),
|
|
626
|
+
{
|
|
627
|
+
name: 'theme',
|
|
628
|
+
storage: createMMKVStorage(),
|
|
629
|
+
}
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// 5. Estado Persistido (MMKV)
|
|
634
|
+
// Para armazenamento de alto desempenho
|
|
635
|
+
// services/storage/mmkv.storage.ts
|
|
636
|
+
import { MMKV } from 'react-native-mmkv';
|
|
637
|
+
|
|
638
|
+
const storage = new MMKV();
|
|
639
|
+
|
|
640
|
+
export const mmkvStorage = {
|
|
641
|
+
setItem: (key: string, value: string) => {
|
|
642
|
+
storage.set(key, value);
|
|
643
|
+
},
|
|
644
|
+
getItem: (key: string) => {
|
|
645
|
+
return storage.getString(key) ?? null;
|
|
646
|
+
},
|
|
647
|
+
removeItem: (key: string) => {
|
|
648
|
+
storage.delete(key);
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
export const createMMKVStorage = () => ({
|
|
653
|
+
getItem: (name: string) => {
|
|
654
|
+
const value = storage.getString(name);
|
|
655
|
+
return value ?? null;
|
|
656
|
+
},
|
|
657
|
+
setItem: (name: string, value: string) => {
|
|
658
|
+
storage.set(name, value);
|
|
659
|
+
},
|
|
660
|
+
removeItem: (name: string) => {
|
|
661
|
+
storage.delete(name);
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### 3. Camada de Domínio (Lógica de Negócio)
|
|
669
|
+
|
|
670
|
+
#### Tipos e Interfaces
|
|
671
|
+
|
|
672
|
+
**Organização de tipos**:
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
// types/User.types.ts
|
|
676
|
+
export interface User {
|
|
677
|
+
id: string;
|
|
678
|
+
email: string;
|
|
679
|
+
firstName: string;
|
|
680
|
+
lastName: string;
|
|
681
|
+
avatar?: string;
|
|
682
|
+
createdAt: Date;
|
|
683
|
+
updatedAt: Date;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export interface CreateUserDTO {
|
|
687
|
+
email: string;
|
|
688
|
+
password: string;
|
|
689
|
+
firstName: string;
|
|
690
|
+
lastName: string;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export interface UpdateUserDTO extends Partial<CreateUserDTO> {
|
|
694
|
+
avatar?: string;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// types/Article.types.ts
|
|
698
|
+
export interface Article {
|
|
699
|
+
id: string;
|
|
700
|
+
title: string;
|
|
701
|
+
content: string;
|
|
702
|
+
author: User;
|
|
703
|
+
tags: string[];
|
|
704
|
+
publishedAt: Date;
|
|
705
|
+
updatedAt: Date;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export interface ArticleFilters {
|
|
709
|
+
tag?: string;
|
|
710
|
+
authorId?: string;
|
|
711
|
+
search?: string;
|
|
712
|
+
page?: number;
|
|
713
|
+
limit?: number;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// types/api.types.ts
|
|
717
|
+
export interface ApiResponse<T> {
|
|
718
|
+
data: T;
|
|
719
|
+
message?: string;
|
|
720
|
+
status: number;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export interface PaginatedResponse<T> {
|
|
724
|
+
data: T[];
|
|
725
|
+
total: number;
|
|
726
|
+
page: number;
|
|
727
|
+
limit: number;
|
|
728
|
+
hasMore: boolean;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export interface ApiError {
|
|
732
|
+
message: string;
|
|
733
|
+
code: string;
|
|
734
|
+
status: number;
|
|
735
|
+
errors?: Record<string, string[]>;
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
### 4. Camada de Dados
|
|
742
|
+
|
|
743
|
+
#### A. Serviços de API
|
|
744
|
+
|
|
745
|
+
**Organização do serviço de API**:
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
// services/api/client.ts
|
|
749
|
+
import axios from 'axios';
|
|
750
|
+
import { setupInterceptors } from './interceptors';
|
|
751
|
+
import { ENV } from '@/config/env';
|
|
752
|
+
|
|
753
|
+
export const apiClient = axios.create({
|
|
754
|
+
baseURL: ENV.API_URL,
|
|
755
|
+
timeout: 10000,
|
|
756
|
+
headers: {
|
|
757
|
+
'Content-Type': 'application/json',
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
setupInterceptors(apiClient);
|
|
762
|
+
|
|
763
|
+
export default apiClient;
|
|
764
|
+
|
|
765
|
+
// services/api/interceptors.ts
|
|
766
|
+
import type { AxiosInstance } from 'axios';
|
|
767
|
+
import { router } from 'expo-router';
|
|
768
|
+
import { tokenService } from '@/features/auth/services/token.service';
|
|
769
|
+
|
|
770
|
+
export const setupInterceptors = (instance: AxiosInstance) => {
|
|
771
|
+
// Interceptador de requisição
|
|
772
|
+
instance.interceptors.request.use(
|
|
773
|
+
async (config) => {
|
|
774
|
+
const token = await tokenService.getAccessToken();
|
|
775
|
+
if (token) {
|
|
776
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
777
|
+
}
|
|
778
|
+
return config;
|
|
779
|
+
},
|
|
780
|
+
(error) => Promise.reject(error)
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
// Interceptador de resposta
|
|
784
|
+
instance.interceptors.response.use(
|
|
785
|
+
(response) => response,
|
|
786
|
+
async (error) => {
|
|
787
|
+
const originalRequest = error.config;
|
|
788
|
+
|
|
789
|
+
// Lógica de renovação de token
|
|
790
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
791
|
+
originalRequest._retry = true;
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const newToken = await tokenService.refreshToken();
|
|
795
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
796
|
+
return instance(originalRequest);
|
|
797
|
+
} catch (refreshError) {
|
|
798
|
+
// Redirecionar para login
|
|
799
|
+
router.replace('/(auth)/login');
|
|
800
|
+
return Promise.reject(refreshError);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return Promise.reject(error);
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// features/articles/services/articles.service.ts
|
|
810
|
+
import { apiClient } from '@/services/api/client';
|
|
811
|
+
import type { Article, ArticleFilters, CreateArticleDTO } from '../types/Article.types';
|
|
812
|
+
import type { PaginatedResponse } from '@/types/api.types';
|
|
813
|
+
|
|
814
|
+
class ArticlesService {
|
|
815
|
+
private readonly endpoint = '/articles';
|
|
816
|
+
|
|
817
|
+
async getAll(filters?: ArticleFilters): Promise<PaginatedResponse<Article>> {
|
|
818
|
+
const { data } = await apiClient.get(this.endpoint, { params: filters });
|
|
819
|
+
return data;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async getById(id: string): Promise<Article> {
|
|
823
|
+
const { data } = await apiClient.get(`${this.endpoint}/${id}`);
|
|
824
|
+
return data;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async create(dto: CreateArticleDTO): Promise<Article> {
|
|
828
|
+
const { data } = await apiClient.post(this.endpoint, dto);
|
|
829
|
+
return data;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async update(id: string, dto: Partial<CreateArticleDTO>): Promise<Article> {
|
|
833
|
+
const { data } = await apiClient.patch(`${this.endpoint}/${id}`, dto);
|
|
834
|
+
return data;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async delete(id: string): Promise<void> {
|
|
838
|
+
await apiClient.delete(`${this.endpoint}/${id}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export const articlesService = new ArticlesService();
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
#### B. Serviços de Armazenamento
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
// services/storage/secure.storage.ts
|
|
849
|
+
import * as SecureStore from 'expo-secure-store';
|
|
850
|
+
import { Platform } from 'react-native';
|
|
851
|
+
|
|
852
|
+
class SecureStorage {
|
|
853
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
854
|
+
if (Platform.OS === 'web') {
|
|
855
|
+
// Fallback para web
|
|
856
|
+
localStorage.setItem(key, value);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
await SecureStore.setItemAsync(key, value);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async getItem(key: string): Promise<string | null> {
|
|
863
|
+
if (Platform.OS === 'web') {
|
|
864
|
+
return localStorage.getItem(key);
|
|
865
|
+
}
|
|
866
|
+
return await SecureStore.getItemAsync(key);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async removeItem(key: string): Promise<void> {
|
|
870
|
+
if (Platform.OS === 'web') {
|
|
871
|
+
localStorage.removeItem(key);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
await SecureStore.deleteItemAsync(key);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export const secureStorage = new SecureStorage();
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Arquitetura de Navegação
|
|
884
|
+
|
|
885
|
+
### Padrões do Expo Router
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
// 1. Rotas de Grupo (Layouts Compartilhados)
|
|
889
|
+
app/
|
|
890
|
+
├── (auth)/
|
|
891
|
+
│ ├── _layout.tsx # Layout de auth
|
|
892
|
+
│ ├── login.tsx
|
|
893
|
+
│ └── register.tsx
|
|
894
|
+
└── (tabs)/
|
|
895
|
+
├── _layout.tsx # Layout de abas
|
|
896
|
+
├── index.tsx
|
|
897
|
+
└── profile.tsx
|
|
898
|
+
|
|
899
|
+
// 2. Rotas Dinâmicas
|
|
900
|
+
app/
|
|
901
|
+
└── article/
|
|
902
|
+
└── [id].tsx # /article/123
|
|
903
|
+
|
|
904
|
+
// 3. Rotas Catch-all
|
|
905
|
+
app/
|
|
906
|
+
└── blog/
|
|
907
|
+
└── [...slug].tsx # /blog/2024/01/article
|
|
908
|
+
|
|
909
|
+
// 4. Rotas de Modal
|
|
910
|
+
app/
|
|
911
|
+
├── _layout.tsx
|
|
912
|
+
└── modal.tsx # Apresentado como modal
|
|
913
|
+
|
|
914
|
+
// Tipos de navegação
|
|
915
|
+
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
|
916
|
+
|
|
917
|
+
type RootStackParamList = {
|
|
918
|
+
'(tabs)': undefined;
|
|
919
|
+
'(auth)': undefined;
|
|
920
|
+
'article/[id]': { id: string };
|
|
921
|
+
modal: { title: string };
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
type ArticleScreenProps = NativeStackScreenProps<RootStackParamList, 'article/[id]'>;
|
|
925
|
+
|
|
926
|
+
// Navegação com tipagem segura
|
|
927
|
+
import { router } from 'expo-router';
|
|
928
|
+
|
|
929
|
+
router.push({ pathname: '/article/[id]', params: { id: '123' } });
|
|
930
|
+
router.back();
|
|
931
|
+
router.replace('/login');
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
## Código Específico por Plataforma
|
|
937
|
+
|
|
938
|
+
### Organização de Código Específico por Plataforma
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
// Método 1: Platform.select
|
|
942
|
+
import { Platform, StyleSheet } from 'react-native';
|
|
943
|
+
|
|
944
|
+
const styles = StyleSheet.create({
|
|
945
|
+
container: {
|
|
946
|
+
...Platform.select({
|
|
947
|
+
ios: {
|
|
948
|
+
paddingTop: 20,
|
|
949
|
+
},
|
|
950
|
+
android: {
|
|
951
|
+
paddingTop: 0,
|
|
952
|
+
},
|
|
953
|
+
default: {
|
|
954
|
+
paddingTop: 10,
|
|
955
|
+
},
|
|
956
|
+
}),
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Método 2: Extensões de arquivo
|
|
961
|
+
// Button.ios.tsx
|
|
962
|
+
export const Button = () => <IOSButton />;
|
|
963
|
+
|
|
964
|
+
// Button.android.tsx
|
|
965
|
+
export const Button = () => <AndroidButton />;
|
|
966
|
+
|
|
967
|
+
// Button.tsx (fallback)
|
|
968
|
+
export const Button = () => <DefaultButton />;
|
|
969
|
+
|
|
970
|
+
// Método 3: Verificação de plataforma
|
|
971
|
+
if (Platform.OS === 'ios') {
|
|
972
|
+
// Específico para iOS
|
|
973
|
+
} else if (Platform.OS === 'android') {
|
|
974
|
+
// Específico para Android
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Método 4: Versão da plataforma
|
|
978
|
+
if (Platform.Version >= 23) {
|
|
979
|
+
// Android API 23+
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## Organização de Módulos Nativos
|
|
986
|
+
|
|
987
|
+
```typescript
|
|
988
|
+
// src/modules/
|
|
989
|
+
├── camera/
|
|
990
|
+
│ ├── CameraModule.ts # Wrapper TypeScript
|
|
991
|
+
│ ├── CameraModule.ios.ts # Implementação iOS
|
|
992
|
+
│ └── CameraModule.android.ts # Implementação Android
|
|
993
|
+
└── biometrics/
|
|
994
|
+
├── BiometricsModule.ts
|
|
995
|
+
├── BiometricsModule.ios.ts
|
|
996
|
+
└── BiometricsModule.android.ts
|
|
997
|
+
|
|
998
|
+
// Exemplo de wrapper
|
|
999
|
+
// modules/camera/CameraModule.ts
|
|
1000
|
+
import { NativeModules } from 'react-native';
|
|
1001
|
+
|
|
1002
|
+
interface CameraInterface {
|
|
1003
|
+
takePicture(): Promise<string>;
|
|
1004
|
+
hasPermission(): Promise<boolean>;
|
|
1005
|
+
requestPermission(): Promise<boolean>;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const { CameraModule } = NativeModules;
|
|
1009
|
+
|
|
1010
|
+
export default CameraModule as CameraInterface;
|
|
1011
|
+
|
|
1012
|
+
// Uso
|
|
1013
|
+
import CameraModule from '@/modules/camera/CameraModule';
|
|
1014
|
+
|
|
1015
|
+
const takePicture = async () => {
|
|
1016
|
+
const hasPermission = await CameraModule.hasPermission();
|
|
1017
|
+
if (!hasPermission) {
|
|
1018
|
+
const granted = await CameraModule.requestPermission();
|
|
1019
|
+
if (!granted) return;
|
|
1020
|
+
}
|
|
1021
|
+
const photoUri = await CameraModule.takePicture();
|
|
1022
|
+
return photoUri;
|
|
1023
|
+
};
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
## Boas Práticas de Arquitetura
|
|
1029
|
+
|
|
1030
|
+
### 1. Injeção de Dependência
|
|
1031
|
+
|
|
1032
|
+
```typescript
|
|
1033
|
+
// ✅ BOM: Injeção de dependência
|
|
1034
|
+
interface StorageService {
|
|
1035
|
+
get(key: string): Promise<string | null>;
|
|
1036
|
+
set(key: string, value: string): Promise<void>;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
class UserRepository {
|
|
1040
|
+
constructor(private storage: StorageService) {}
|
|
1041
|
+
|
|
1042
|
+
async getUser(): Promise<User | null> {
|
|
1043
|
+
const data = await this.storage.get('user');
|
|
1044
|
+
return data ? JSON.parse(data) : null;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Uso
|
|
1049
|
+
const repository = new UserRepository(mmkvStorage);
|
|
1050
|
+
|
|
1051
|
+
// ❌ RUIM: Dependência hard-coded
|
|
1052
|
+
class UserRepository {
|
|
1053
|
+
async getUser() {
|
|
1054
|
+
const data = await mmkvStorage.get('user'); // Acoplamento forte
|
|
1055
|
+
return data ? JSON.parse(data) : null;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
### 2. Padrão Repository
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
// features/articles/repositories/articles.repository.ts
|
|
1064
|
+
import { articlesService } from '../services/articles.service';
|
|
1065
|
+
import { articlesStorage } from '../services/articles.storage';
|
|
1066
|
+
|
|
1067
|
+
class ArticlesRepository {
|
|
1068
|
+
async getArticles(filters?: ArticleFilters): Promise<Article[]> {
|
|
1069
|
+
try {
|
|
1070
|
+
// Tenta a API primeiro
|
|
1071
|
+
const articles = await articlesService.getAll(filters);
|
|
1072
|
+
// Salva em cache local
|
|
1073
|
+
await articlesStorage.saveArticles(articles.data);
|
|
1074
|
+
return articles.data;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
// Fallback para armazenamento local
|
|
1077
|
+
return await articlesStorage.getArticles();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async getArticleById(id: string): Promise<Article | null> {
|
|
1082
|
+
// Tenta o cache primeiro
|
|
1083
|
+
const cached = await articlesStorage.getArticleById(id);
|
|
1084
|
+
if (cached) return cached;
|
|
1085
|
+
|
|
1086
|
+
// Busca da API
|
|
1087
|
+
const article = await articlesService.getById(id);
|
|
1088
|
+
await articlesStorage.saveArticle(article);
|
|
1089
|
+
return article;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export const articlesRepository = new ArticlesRepository();
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
### 3. Padrão Adapter
|
|
1097
|
+
|
|
1098
|
+
```typescript
|
|
1099
|
+
// Para adaptar APIs externas
|
|
1100
|
+
interface ArticleDTO {
|
|
1101
|
+
id: number;
|
|
1102
|
+
post_title: string;
|
|
1103
|
+
post_content: string;
|
|
1104
|
+
author_name: string;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
interface Article {
|
|
1108
|
+
id: string;
|
|
1109
|
+
title: string;
|
|
1110
|
+
content: string;
|
|
1111
|
+
author: string;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
class ArticleAdapter {
|
|
1115
|
+
static toArticle(dto: ArticleDTO): Article {
|
|
1116
|
+
return {
|
|
1117
|
+
id: String(dto.id),
|
|
1118
|
+
title: dto.post_title,
|
|
1119
|
+
content: dto.post_content,
|
|
1120
|
+
author: dto.author_name,
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
static toDTO(article: Article): ArticleDTO {
|
|
1125
|
+
return {
|
|
1126
|
+
id: Number(article.id),
|
|
1127
|
+
post_title: article.title,
|
|
1128
|
+
post_content: article.content,
|
|
1129
|
+
author_name: article.author,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
---
|
|
1136
|
+
|
|
1137
|
+
## Exemplos Completos
|
|
1138
|
+
|
|
1139
|
+
### Exemplo Completo de Funcionalidade: Artigos
|
|
1140
|
+
|
|
1141
|
+
```typescript
|
|
1142
|
+
// 1. Tipos
|
|
1143
|
+
// features/articles/types/Article.types.ts
|
|
1144
|
+
export interface Article {
|
|
1145
|
+
id: string;
|
|
1146
|
+
title: string;
|
|
1147
|
+
content: string;
|
|
1148
|
+
authorId: string;
|
|
1149
|
+
tags: string[];
|
|
1150
|
+
publishedAt: Date;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// 2. Serviço de API
|
|
1154
|
+
// features/articles/services/articles.service.ts
|
|
1155
|
+
class ArticlesService {
|
|
1156
|
+
async getAll(): Promise<Article[]> {
|
|
1157
|
+
const { data } = await apiClient.get('/articles');
|
|
1158
|
+
return data;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
export const articlesService = new ArticlesService();
|
|
1162
|
+
|
|
1163
|
+
// 3. Serviço de Armazenamento
|
|
1164
|
+
// features/articles/services/articles.storage.ts
|
|
1165
|
+
class ArticlesStorage {
|
|
1166
|
+
async saveArticles(articles: Article[]): Promise<void> {
|
|
1167
|
+
await mmkvStorage.setItem('articles', JSON.stringify(articles));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async getArticles(): Promise<Article[]> {
|
|
1171
|
+
const data = await mmkvStorage.getItem('articles');
|
|
1172
|
+
return data ? JSON.parse(data) : [];
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
export const articlesStorage = new ArticlesStorage();
|
|
1176
|
+
|
|
1177
|
+
// 4. Repository
|
|
1178
|
+
// features/articles/repositories/articles.repository.ts
|
|
1179
|
+
class ArticlesRepository {
|
|
1180
|
+
async getArticles(): Promise<Article[]> {
|
|
1181
|
+
try {
|
|
1182
|
+
const articles = await articlesService.getAll();
|
|
1183
|
+
await articlesStorage.saveArticles(articles);
|
|
1184
|
+
return articles;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return await articlesStorage.getArticles();
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
export const articlesRepository = new ArticlesRepository();
|
|
1191
|
+
|
|
1192
|
+
// 5. Hook
|
|
1193
|
+
// features/articles/hooks/useArticles.ts
|
|
1194
|
+
export const useArticles = () => {
|
|
1195
|
+
return useQuery({
|
|
1196
|
+
queryKey: ['articles'],
|
|
1197
|
+
queryFn: () => articlesRepository.getArticles(),
|
|
1198
|
+
});
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// 6. Componente
|
|
1202
|
+
// features/articles/components/ArticleCard/ArticleCard.tsx
|
|
1203
|
+
interface ArticleCardProps {
|
|
1204
|
+
article: Article;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
export const ArticleCard = ({ article }: ArticleCardProps) => {
|
|
1208
|
+
return (
|
|
1209
|
+
<Card onPress={() => router.push(`/article/${article.id}`)}>
|
|
1210
|
+
<Text style={styles.title}>{article.title}</Text>
|
|
1211
|
+
<Text style={styles.content}>{article.content}</Text>
|
|
1212
|
+
</Card>
|
|
1213
|
+
);
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// 7. Tela
|
|
1217
|
+
// app/(tabs)/articles.tsx
|
|
1218
|
+
export default function ArticlesScreen() {
|
|
1219
|
+
const { data: articles, isLoading } = useArticles();
|
|
1220
|
+
|
|
1221
|
+
if (isLoading) return <LoadingSpinner />;
|
|
1222
|
+
|
|
1223
|
+
return (
|
|
1224
|
+
<FlatList
|
|
1225
|
+
data={articles}
|
|
1226
|
+
renderItem={({ item }) => <ArticleCard article={item} />}
|
|
1227
|
+
keyExtractor={(item) => item.id}
|
|
1228
|
+
/>
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
180
1233
|
---
|
|
181
1234
|
|
|
182
1235
|
## Checklist de Arquitetura
|
|
183
1236
|
|
|
184
|
-
**Antes de implementar uma
|
|
1237
|
+
**Antes de implementar uma funcionalidade**:
|
|
185
1238
|
|
|
186
|
-
- [ ] Pasta
|
|
1239
|
+
- [ ] Pasta da funcionalidade criada em features/
|
|
187
1240
|
- [ ] Tipos definidos em types/
|
|
188
|
-
- [ ] Serviço API criado em services/
|
|
189
|
-
- [ ] Serviço de
|
|
190
|
-
- [ ] Repository se lógica complexa
|
|
191
|
-
- [ ]
|
|
1241
|
+
- [ ] Serviço de API criado em services/
|
|
1242
|
+
- [ ] Serviço de armazenamento se necessário
|
|
1243
|
+
- [ ] Repository se houver lógica complexa
|
|
1244
|
+
- [ ] Custom hook criado em hooks/
|
|
192
1245
|
- [ ] Componentes UI em components/
|
|
193
|
-
- [ ]
|
|
1246
|
+
- [ ] Tela em app/
|
|
194
1247
|
- [ ] Navegação configurada
|
|
195
1248
|
- [ ] Testes unitários
|
|
196
1249
|
- [ ] Documentação
|
|
197
1250
|
|
|
198
1251
|
---
|
|
199
1252
|
|
|
200
|
-
**
|
|
1253
|
+
**A arquitetura é a base da manutenibilidade. Invista tempo desde o início.**
|