@buivietphi/skill-mobile-mt 2.0.1 → 2.1.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 +56 -38
- package/README.md +68 -40
- package/SKILL.md +471 -50
- package/package.json +1 -1
- package/shared/bug-detection.md +411 -27
- package/shared/code-review.md +899 -37
- package/shared/debugging-intelligence.md +787 -0
- package/shared/i18n-localization.md +426 -0
- package/shared/prompt-engineering.md +176 -20
- package/shared/storage-patterns.md +312 -0
|
@@ -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
|
+
```
|