@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.
@@ -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
+ ```