@buivietphi/skill-mobile-mt 2.0.1 → 2.2.0
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/AGENTS.md +96 -40
- package/README.md +77 -40
- package/SKILL.md +762 -54
- package/package.json +1 -1
- package/shared/bug-detection.md +411 -27
- package/shared/code-generation-templates.md +656 -0
- package/shared/code-review.md +899 -37
- package/shared/complex-ui-patterns.md +526 -0
- package/shared/data-flow-patterns.md +422 -0
- package/shared/debugging-intelligence.md +787 -0
- package/shared/error-handling.md +394 -0
- package/shared/i18n-localization.md +426 -0
- package/shared/intent-analysis.md +473 -0
- package/shared/navigation-patterns.md +375 -0
- package/shared/prompt-engineering.md +176 -20
- package/shared/spec-to-code.md +293 -0
- package/shared/storage-patterns.md +312 -0
- package/shared/testing-patterns.md +428 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Spec-to-Code — From Requirements to Implementation
|
|
2
|
+
|
|
3
|
+
> On-demand module. Loaded when building new features from specs, user stories, or vague descriptions.
|
|
4
|
+
> Bridges the gap between "what to build" and "how to implement it".
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Spec → Code Pipeline
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
STEP 1: PARSE SPEC (extract structured requirements)
|
|
12
|
+
STEP 2: DEPENDENCY GRAPH (map what depends on what)
|
|
13
|
+
STEP 3: FILE PLAN (which files to create/modify)
|
|
14
|
+
STEP 4: TYPE DEFINITIONS (interfaces + branded types)
|
|
15
|
+
STEP 5: IMPLEMENT (bottom-up: types → services → hooks → screens)
|
|
16
|
+
STEP 6: VERIFY (against original spec checklist)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Step 1: Parse Spec → Structured Requirements
|
|
22
|
+
|
|
23
|
+
Given ANY feature description, extract these 8 items:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌─────────────────────────────────────────┐
|
|
27
|
+
│ 1. ENTITY What data objects? │
|
|
28
|
+
│ 2. FIELDS What properties each? │
|
|
29
|
+
│ 3. ACTIONS What can user do? │
|
|
30
|
+
│ 4. STATES Loading/error/empty/ok │
|
|
31
|
+
│ 5. NAVIGATION From where? To where? │
|
|
32
|
+
│ 6. API Which endpoints? │
|
|
33
|
+
│ 7. STORAGE Persist anything local? │
|
|
34
|
+
│ 8. VALIDATION Input rules? │
|
|
35
|
+
└─────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Step 2: Dependency Graph Template
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
[FeatureName]Screen
|
|
44
|
+
├── Components
|
|
45
|
+
│ ├── [Name]Header
|
|
46
|
+
│ ├── [Name]List / [Name]Card
|
|
47
|
+
│ ├── [Name]Form (if editable)
|
|
48
|
+
│ └── [Name]Empty / [Name]Error / [Name]Skeleton
|
|
49
|
+
│
|
|
50
|
+
├── Hook: use[FeatureName]
|
|
51
|
+
│ ├── Query: use[Entity]Query (GET data)
|
|
52
|
+
│ ├── Mutation: use[Action]Mutation (POST/PUT/DELETE)
|
|
53
|
+
│ └── State: use[Store]Store (local state)
|
|
54
|
+
│
|
|
55
|
+
├── Service: [entity]Service.ts
|
|
56
|
+
│ ├── get[Entity](params) → API call
|
|
57
|
+
│ ├── create[Entity](data) → API call
|
|
58
|
+
│ ├── update[Entity](id, data) → API call
|
|
59
|
+
│ └── delete[Entity](id) → API call
|
|
60
|
+
│
|
|
61
|
+
├── Types: [entity].types.ts
|
|
62
|
+
│ ├── [Entity] interface
|
|
63
|
+
│ ├── Create[Entity]Input
|
|
64
|
+
│ ├── Update[Entity]Input
|
|
65
|
+
│ └── [Entity]Params (filters, pagination)
|
|
66
|
+
│
|
|
67
|
+
└── Navigation: registered in navigator
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 3: File Plan — What Goes Where
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
RULE: Follow existing project structure. NEVER invent new patterns.
|
|
76
|
+
RULE: Scan project for a SIMILAR feature. Clone its file structure.
|
|
77
|
+
|
|
78
|
+
TYPICAL FILE PLAN:
|
|
79
|
+
|
|
80
|
+
src/features/[feature]/
|
|
81
|
+
├── [Feature]Screen.tsx ← Screen component (4 states)
|
|
82
|
+
├── components/
|
|
83
|
+
│ ├── [Feature]Header.tsx ← Header with title + actions
|
|
84
|
+
│ ├── [Feature]List.tsx ← List/grid of items
|
|
85
|
+
│ ├── [Feature]Card.tsx ← Single item card
|
|
86
|
+
│ ├── [Feature]Form.tsx ← Form (if editable)
|
|
87
|
+
│ ├── [Feature]Skeleton.tsx ← Loading skeleton
|
|
88
|
+
│ └── [Feature]Empty.tsx ← Empty state
|
|
89
|
+
├── hooks/
|
|
90
|
+
│ └── use[Feature].ts ← Business logic hook
|
|
91
|
+
├── services/
|
|
92
|
+
│ └── [feature]Service.ts ← API calls
|
|
93
|
+
└── types/
|
|
94
|
+
└── [feature].types.ts ← TypeScript interfaces
|
|
95
|
+
|
|
96
|
+
ALSO UPDATE:
|
|
97
|
+
├── navigation/ ← Register new screen
|
|
98
|
+
└── stores/ (if new store) ← Only if feature needs global state
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Step 4: Type-First Development
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
ALWAYS write types BEFORE implementation.
|
|
107
|
+
|
|
108
|
+
ORDER:
|
|
109
|
+
1. Entity types (what data looks like)
|
|
110
|
+
2. Input types (what user submits)
|
|
111
|
+
3. API response types (what server returns)
|
|
112
|
+
4. Screen param types (navigation params)
|
|
113
|
+
|
|
114
|
+
WHY: Types catch integration errors before runtime.
|
|
115
|
+
Types serve as documentation for the feature.
|
|
116
|
+
Types make code review faster (reviewer reads types first).
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Step 5: Implementation Order (Bottom-Up)
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
WRONG ORDER (causes integration bugs):
|
|
125
|
+
Screen → Hook → Service → Types
|
|
126
|
+
(screen written before knowing what data looks like)
|
|
127
|
+
|
|
128
|
+
RIGHT ORDER:
|
|
129
|
+
1. types/[feature].types.ts ← Define the contract
|
|
130
|
+
2. services/[feature]Service.ts ← Implement API calls
|
|
131
|
+
3. hooks/use[Feature].ts ← Wire service + state
|
|
132
|
+
4. components/ ← Build UI pieces
|
|
133
|
+
5. [Feature]Screen.tsx ← Compose everything
|
|
134
|
+
6. navigation/ ← Register route
|
|
135
|
+
|
|
136
|
+
Each step VERIFIES against the previous:
|
|
137
|
+
Service matches types? ✓
|
|
138
|
+
Hook calls service correctly? ✓
|
|
139
|
+
Component renders hook data? ✓
|
|
140
|
+
Screen composes components? ✓
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Full Walkthrough Example
|
|
146
|
+
|
|
147
|
+
### Spec: "Product Detail Screen"
|
|
148
|
+
|
|
149
|
+
**User says:** "I need a product detail screen showing images, title, price, description, reviews, and an 'Add to Cart' button. Cart persists offline."
|
|
150
|
+
|
|
151
|
+
### Parse:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
1. ENTITY: Product, CartItem, Review
|
|
155
|
+
2. FIELDS:
|
|
156
|
+
Product → id, title, price, description, images[], category, inStock
|
|
157
|
+
CartItem → productId, quantity, price
|
|
158
|
+
Review → id, userId, rating, comment, createdAt
|
|
159
|
+
3. ACTIONS: View product, Add to cart, View reviews, Share
|
|
160
|
+
4. STATES: Loading (skeleton), Error (retry), Empty (404), Success
|
|
161
|
+
5. NAVIGATION: From: ProductList → To: Cart, ReviewList
|
|
162
|
+
6. API: GET /products/:id, POST /cart/items, GET /products/:id/reviews
|
|
163
|
+
7. STORAGE: Cart stored locally (MMKV) for offline
|
|
164
|
+
8. VALIDATION: Quantity ≥ 1, max 99
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Dependency Graph:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
ProductDetailScreen
|
|
171
|
+
├── ImageCarousel ← Horizontal scroll, snap, indicators
|
|
172
|
+
├── ProductInfo ← Title, price, description, stock badge
|
|
173
|
+
├── ReviewSummary ← Average rating, count, "See all" link
|
|
174
|
+
├── AddToCartButton ← Quantity selector + CTA
|
|
175
|
+
│
|
|
176
|
+
├── useProductDetail(id)
|
|
177
|
+
│ ├── useQuery(['product', id], () => productService.getById(id))
|
|
178
|
+
│ └── useQuery(['reviews', id], () => productService.getReviews(id))
|
|
179
|
+
│
|
|
180
|
+
├── useCartStore (Zustand + MMKV persist)
|
|
181
|
+
│ ├── addItem(productId, quantity, price)
|
|
182
|
+
│ ├── removeItem(productId)
|
|
183
|
+
│ └── items: CartItem[]
|
|
184
|
+
│
|
|
185
|
+
├── productService.ts
|
|
186
|
+
│ ├── getById(id: ProductId): Promise<Product>
|
|
187
|
+
│ └── getReviews(id: ProductId): Promise<Review[]>
|
|
188
|
+
│
|
|
189
|
+
└── Types
|
|
190
|
+
├── Product, CartItem, Review
|
|
191
|
+
├── ProductDetailParams = { productId: ProductId }
|
|
192
|
+
└── AddToCartInput = { productId: ProductId; quantity: number }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### File Plan:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
src/features/product/
|
|
199
|
+
├── ProductDetailScreen.tsx
|
|
200
|
+
├── components/
|
|
201
|
+
│ ├── ImageCarousel.tsx
|
|
202
|
+
│ ├── ProductInfo.tsx
|
|
203
|
+
│ ├── ReviewSummary.tsx
|
|
204
|
+
│ ├── AddToCartButton.tsx
|
|
205
|
+
│ └── ProductDetailSkeleton.tsx
|
|
206
|
+
├── hooks/
|
|
207
|
+
│ └── useProductDetail.ts
|
|
208
|
+
├── services/
|
|
209
|
+
│ └── productService.ts
|
|
210
|
+
└── types/
|
|
211
|
+
└── product.types.ts
|
|
212
|
+
|
|
213
|
+
src/stores/useCartStore.ts ← Global (shared across features)
|
|
214
|
+
navigation/types.ts ← Add ProductDetail params
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Implementation (abbreviated — types first):
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// 1. types/product.types.ts
|
|
221
|
+
export interface Product {
|
|
222
|
+
id: ProductId;
|
|
223
|
+
title: string;
|
|
224
|
+
price: number;
|
|
225
|
+
description: string;
|
|
226
|
+
images: string[];
|
|
227
|
+
category: string;
|
|
228
|
+
inStock: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface Review {
|
|
232
|
+
id: string;
|
|
233
|
+
userId: UserId;
|
|
234
|
+
rating: number; // 1-5
|
|
235
|
+
comment: string;
|
|
236
|
+
createdAt: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface AddToCartInput {
|
|
240
|
+
productId: ProductId;
|
|
241
|
+
quantity: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2. services/productService.ts
|
|
245
|
+
export const productService = {
|
|
246
|
+
getById: (id: ProductId) => api.get<Product>(`/products/${id}`),
|
|
247
|
+
getReviews: (id: ProductId) => api.get<Review[]>(`/products/${id}/reviews`),
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// 3. hooks/useProductDetail.ts
|
|
251
|
+
export function useProductDetail(productId: ProductId) {
|
|
252
|
+
const product = useQuery({ queryKey: ['product', productId], queryFn: () => productService.getById(productId) });
|
|
253
|
+
const reviews = useQuery({ queryKey: ['reviews', productId], queryFn: () => productService.getReviews(productId) });
|
|
254
|
+
const addToCart = useCartStore(state => state.addItem);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
product: product.data,
|
|
258
|
+
reviews: reviews.data,
|
|
259
|
+
isLoading: product.isLoading,
|
|
260
|
+
error: product.error,
|
|
261
|
+
refetch: product.refetch,
|
|
262
|
+
handleAddToCart: (quantity: number) => {
|
|
263
|
+
if (!product.data) return;
|
|
264
|
+
addToCart(product.data.id, quantity, product.data.price);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 4-5. Screen composes hook + components with 4 states
|
|
270
|
+
// → Loading: <ProductDetailSkeleton />
|
|
271
|
+
// → Error: <ErrorView onRetry={refetch} />
|
|
272
|
+
// → Empty: <NotFoundView />
|
|
273
|
+
// → Success: <ImageCarousel /> + <ProductInfo /> + <ReviewSummary /> + <AddToCartButton />
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Checklist: Verify Against Spec
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
After implementing, check EVERY item from the parsed spec:
|
|
282
|
+
|
|
283
|
+
□ All ENTITIES defined in types?
|
|
284
|
+
□ All FIELDS present in interfaces?
|
|
285
|
+
□ All ACTIONS wired to handlers?
|
|
286
|
+
□ All 4 STATES rendered?
|
|
287
|
+
□ NAVIGATION registered + params typed?
|
|
288
|
+
□ All API endpoints called correctly?
|
|
289
|
+
□ STORAGE persisted where needed?
|
|
290
|
+
□ VALIDATION applied to inputs?
|
|
291
|
+
□ Accessibility labels on interactive elements?
|
|
292
|
+
□ Platform-specific behavior handled (iOS vs Android)?
|
|
293
|
+
```
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Mobile Storage Patterns
|
|
2
|
+
|
|
3
|
+
> On-device storage — when to use what, how to implement correctly.
|
|
4
|
+
> Covers: AsyncStorage, MMKV, SecureStore/Keychain, SQLite, WatermelonDB, Realm, SharedPreferences.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Decision Matrix — Pick Storage Type First
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
WHAT ARE YOU STORING? → STORAGE TO USE
|
|
12
|
+
────────────────────────────────────────────────────────────────────
|
|
13
|
+
Auth tokens / secrets → SecureStore (RN) / Keychain (iOS)
|
|
14
|
+
EncryptedSharedPreferences (Android)
|
|
15
|
+
flutter_secure_storage (Flutter)
|
|
16
|
+
|
|
17
|
+
App preferences / settings → MMKV (RN, fast KV)
|
|
18
|
+
(theme, language, onboarding) SharedPreferences (Android native)
|
|
19
|
+
UserDefaults (iOS native)
|
|
20
|
+
shared_preferences (Flutter)
|
|
21
|
+
|
|
22
|
+
Simple key-value cache → MMKV (RN) / MMKV (Flutter)
|
|
23
|
+
(session data, small objects)
|
|
24
|
+
|
|
25
|
+
Structured relational data → SQLite (via expo-sqlite / sqflite)
|
|
26
|
+
(offline CRUD, complex queries) WatermelonDB (RN, reactive queries)
|
|
27
|
+
drift (Flutter, type-safe)
|
|
28
|
+
|
|
29
|
+
Large offline datasets → WatermelonDB (RN)
|
|
30
|
+
(sync with server, observables) drift (Flutter)
|
|
31
|
+
Room (Android native)
|
|
32
|
+
CoreData / SwiftData (iOS native)
|
|
33
|
+
Realm (cross-platform)
|
|
34
|
+
|
|
35
|
+
Files / images / documents → FileSystem (expo-file-system / path_provider)
|
|
36
|
+
AsyncStorage ❌ (NOT for binary data)
|
|
37
|
+
|
|
38
|
+
⛔ RULE: AsyncStorage is deprecated for RN. Use MMKV instead.
|
|
39
|
+
⛔ RULE: NEVER store tokens in AsyncStorage / SharedPreferences / UserDefaults.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## React Native
|
|
45
|
+
|
|
46
|
+
### 1. Secure Storage (Tokens, Credentials)
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// expo-secure-store (Expo) / react-native-keychain (bare RN)
|
|
50
|
+
import * as SecureStore from 'expo-secure-store';
|
|
51
|
+
|
|
52
|
+
// Store
|
|
53
|
+
await SecureStore.setItemAsync('accessToken', token);
|
|
54
|
+
|
|
55
|
+
// Read
|
|
56
|
+
const token = await SecureStore.getItemAsync('accessToken');
|
|
57
|
+
|
|
58
|
+
// Delete (on logout — ALWAYS do this)
|
|
59
|
+
await SecureStore.deleteItemAsync('accessToken');
|
|
60
|
+
await SecureStore.deleteItemAsync('refreshToken');
|
|
61
|
+
|
|
62
|
+
// RULE: On logout, delete ALL secure store keys
|
|
63
|
+
async function logout() {
|
|
64
|
+
await Promise.all([
|
|
65
|
+
SecureStore.deleteItemAsync('accessToken'),
|
|
66
|
+
SecureStore.deleteItemAsync('refreshToken'),
|
|
67
|
+
SecureStore.deleteItemAsync('userId'),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. MMKV (Preferences + KV Cache) — 60x faster than AsyncStorage
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// react-native-mmkv
|
|
76
|
+
import { MMKV } from 'react-native-mmkv';
|
|
77
|
+
|
|
78
|
+
// Create instance (one per app, or per domain)
|
|
79
|
+
export const storage = new MMKV();
|
|
80
|
+
|
|
81
|
+
// Typed wrapper (recommended)
|
|
82
|
+
export const Storage = {
|
|
83
|
+
getString: (key: string) => storage.getString(key),
|
|
84
|
+
setString: (key: string, value: string) => storage.set(key, value),
|
|
85
|
+
getBoolean: (key: string) => storage.getBoolean(key) ?? false,
|
|
86
|
+
setBoolean: (key: string, value: boolean) => storage.set(key, value),
|
|
87
|
+
getObject: <T>(key: string): T | null => {
|
|
88
|
+
const raw = storage.getString(key);
|
|
89
|
+
return raw ? JSON.parse(raw) : null;
|
|
90
|
+
},
|
|
91
|
+
setObject: <T>(key: string, value: T) => storage.set(key, JSON.stringify(value)),
|
|
92
|
+
delete: (key: string) => storage.delete(key),
|
|
93
|
+
clear: () => storage.clearAll(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// With Zustand persist (recommended combo)
|
|
97
|
+
import { create } from 'zustand';
|
|
98
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
99
|
+
|
|
100
|
+
const mmkvStorage = {
|
|
101
|
+
getItem: (name: string) => storage.getString(name) ?? null,
|
|
102
|
+
setItem: (name: string, value: string) => storage.set(name, value),
|
|
103
|
+
removeItem: (name: string) => storage.delete(name),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const useSettingsStore = create(
|
|
107
|
+
persist(
|
|
108
|
+
(set) => ({
|
|
109
|
+
theme: 'light',
|
|
110
|
+
language: 'en',
|
|
111
|
+
setTheme: (theme: string) => set({ theme }),
|
|
112
|
+
setLanguage: (lang: string) => set({ language: lang }),
|
|
113
|
+
}),
|
|
114
|
+
{ name: 'settings', storage: createJSONStorage(() => mmkvStorage) }
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 3. SQLite / WatermelonDB (Structured Offline Data)
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// expo-sqlite (simple queries)
|
|
123
|
+
import * as SQLite from 'expo-sqlite';
|
|
124
|
+
|
|
125
|
+
const db = SQLite.openDatabaseSync('app.db');
|
|
126
|
+
|
|
127
|
+
// Init schema
|
|
128
|
+
db.execSync(`
|
|
129
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
130
|
+
id TEXT PRIMARY KEY,
|
|
131
|
+
title TEXT NOT NULL,
|
|
132
|
+
completed INTEGER NOT NULL DEFAULT 0,
|
|
133
|
+
created_at INTEGER NOT NULL
|
|
134
|
+
)
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
// Query
|
|
138
|
+
const tasks = db.getAllSync<Task>('SELECT * FROM tasks WHERE completed = ?', [0]);
|
|
139
|
+
|
|
140
|
+
// Insert
|
|
141
|
+
db.runSync('INSERT INTO tasks (id, title, completed, created_at) VALUES (?, ?, ?, ?)',
|
|
142
|
+
[uuid(), 'Buy milk', 0, Date.now()]);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// WatermelonDB (reactive queries, sync-ready)
|
|
147
|
+
// Best for: large datasets, reactive UI, server sync
|
|
148
|
+
import { Database } from '@nozbe/watermelondb';
|
|
149
|
+
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
|
|
150
|
+
|
|
151
|
+
const adapter = new SQLiteAdapter({ schema, migrations });
|
|
152
|
+
const database = new Database({ adapter, modelClasses: [Post, Comment] });
|
|
153
|
+
|
|
154
|
+
// Observe (reactive — auto re-renders on change)
|
|
155
|
+
const posts = database.get('posts').query().observe();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 4. Avoid AsyncStorage (Legacy)
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// ❌ DEPRECATED — avoid in new projects
|
|
162
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
163
|
+
|
|
164
|
+
// ✅ Migrate to MMKV:
|
|
165
|
+
// Before: await AsyncStorage.setItem('theme', 'dark');
|
|
166
|
+
// After: storage.set('theme', 'dark');
|
|
167
|
+
|
|
168
|
+
// ✅ Migrate to expo-secure-store for tokens:
|
|
169
|
+
// Before: await AsyncStorage.setItem('token', jwt);
|
|
170
|
+
// After: await SecureStore.setItemAsync('token', jwt);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Flutter
|
|
176
|
+
|
|
177
|
+
### 1. Secure Storage (Tokens)
|
|
178
|
+
|
|
179
|
+
```dart
|
|
180
|
+
// flutter_secure_storage
|
|
181
|
+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
182
|
+
|
|
183
|
+
final _secureStorage = FlutterSecureStorage(
|
|
184
|
+
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
|
185
|
+
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Store
|
|
189
|
+
await _secureStorage.write(key: 'accessToken', value: token);
|
|
190
|
+
|
|
191
|
+
// Read
|
|
192
|
+
final token = await _secureStorage.read(key: 'accessToken');
|
|
193
|
+
|
|
194
|
+
// Delete on logout
|
|
195
|
+
await _secureStorage.deleteAll();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 2. SharedPreferences / Hive (KV Storage)
|
|
199
|
+
|
|
200
|
+
```dart
|
|
201
|
+
// shared_preferences (simple, built-in)
|
|
202
|
+
final prefs = await SharedPreferences.getInstance();
|
|
203
|
+
await prefs.setString('language', 'en');
|
|
204
|
+
final lang = prefs.getString('language') ?? 'en';
|
|
205
|
+
|
|
206
|
+
// Hive (faster, type-safe, no codegen)
|
|
207
|
+
import 'package:hive_flutter/hive_flutter.dart';
|
|
208
|
+
|
|
209
|
+
await Hive.initFlutter();
|
|
210
|
+
final box = await Hive.openBox('settings');
|
|
211
|
+
box.put('theme', 'dark');
|
|
212
|
+
final theme = box.get('theme', defaultValue: 'light');
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 3. Drift (SQLite, type-safe)
|
|
216
|
+
|
|
217
|
+
```dart
|
|
218
|
+
// drift — type-safe SQLite with code generation
|
|
219
|
+
@DriftDatabase(tables: [Tasks])
|
|
220
|
+
class AppDatabase extends _$AppDatabase {
|
|
221
|
+
AppDatabase() : super(_openConnection());
|
|
222
|
+
|
|
223
|
+
Stream<List<Task>> watchIncompleteTasks() =>
|
|
224
|
+
(select(tasks)..where((t) => t.completed.not())).watch();
|
|
225
|
+
|
|
226
|
+
Future insertTask(TasksCompanion task) => into(tasks).insert(task);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## iOS Native (Swift)
|
|
233
|
+
|
|
234
|
+
```swift
|
|
235
|
+
// UserDefaults — preferences ONLY (not tokens)
|
|
236
|
+
UserDefaults.standard.set("en", forKey: "language")
|
|
237
|
+
let lang = UserDefaults.standard.string(forKey: "language") ?? "en"
|
|
238
|
+
|
|
239
|
+
// Keychain — tokens and secrets
|
|
240
|
+
import Security
|
|
241
|
+
|
|
242
|
+
func saveToKeychain(key: String, value: String) {
|
|
243
|
+
let data = value.data(using: .utf8)!
|
|
244
|
+
let query: [String: Any] = [
|
|
245
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
246
|
+
kSecAttrAccount as String: key,
|
|
247
|
+
kSecValueData as String: data,
|
|
248
|
+
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
249
|
+
]
|
|
250
|
+
SecItemDelete(query as CFDictionary)
|
|
251
|
+
SecItemAdd(query as CFDictionary, nil)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// SwiftData / CoreData — structured offline data
|
|
255
|
+
@Model class Task {
|
|
256
|
+
var id: UUID
|
|
257
|
+
var title: String
|
|
258
|
+
var completed: Bool
|
|
259
|
+
init(title: String) { self.id = UUID(); self.title = title; self.completed = false }
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Android Native (Kotlin)
|
|
266
|
+
|
|
267
|
+
```kotlin
|
|
268
|
+
// EncryptedSharedPreferences — tokens and secrets
|
|
269
|
+
val masterKey = MasterKey.Builder(context)
|
|
270
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
|
271
|
+
val encryptedPrefs = EncryptedSharedPreferences.create(
|
|
272
|
+
context, "secure_prefs", masterKey,
|
|
273
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
274
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
275
|
+
)
|
|
276
|
+
encryptedPrefs.edit().putString("accessToken", token).apply()
|
|
277
|
+
|
|
278
|
+
// DataStore — preferences (replaces SharedPreferences)
|
|
279
|
+
val Context.dataStore by preferencesDataStore(name = "settings")
|
|
280
|
+
val LANGUAGE_KEY = stringPreferencesKey("language")
|
|
281
|
+
|
|
282
|
+
suspend fun saveLanguage(context: Context, lang: String) {
|
|
283
|
+
context.dataStore.edit { it[LANGUAGE_KEY] = lang }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
val languageFlow = context.dataStore.data.map { it[LANGUAGE_KEY] ?: "en" }
|
|
287
|
+
|
|
288
|
+
// Room — structured offline data
|
|
289
|
+
@Entity data class Task(@PrimaryKey val id: String, val title: String, val completed: Boolean)
|
|
290
|
+
@Dao interface TaskDao {
|
|
291
|
+
@Query("SELECT * FROM task WHERE completed = 0") fun getActive(): Flow<List<Task>>
|
|
292
|
+
@Insert suspend fun insert(task: Task)
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Security Checklist
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
✅ Tokens → SecureStore / Keychain / EncryptedSharedPreferences ONLY
|
|
302
|
+
✅ On logout → delete ALL secure storage keys
|
|
303
|
+
✅ Encrypt sensitive data before storing in SQLite/MMKV
|
|
304
|
+
✅ Don't log stored values (console.log, print)
|
|
305
|
+
✅ Use device-only accessibility (not iCloud sync for tokens)
|
|
306
|
+
|
|
307
|
+
⛔ NEVER: AsyncStorage for tokens
|
|
308
|
+
⛔ NEVER: UserDefaults for tokens
|
|
309
|
+
⛔ NEVER: SharedPreferences (unencrypted) for tokens
|
|
310
|
+
⛔ NEVER: Log token values in debug output
|
|
311
|
+
⛔ NEVER: Store plain-text passwords
|
|
312
|
+
```
|