@buivietphi/skill-mobile-mt 2.0.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,377 @@
1
+ # Offline-First — Mobile Data Strategy
2
+
3
+ > On-demand. Load when: "offline", "offline-first", "cache", "sync", "local database", "persistence"
4
+ > Source: Mattermost, Immich, Expensify, Ignite
5
+
6
+ ---
7
+
8
+ ## Architecture
9
+
10
+ ```
11
+ ┌─────────────────────────────────────┐
12
+ │ UI Layer (reads local only) │
13
+ └──────────────┬──────────────────────┘
14
+
15
+ ┌──────────────▼──────────────────────┐
16
+ │ Repository Layer (sync logic) │
17
+ └──────┬──────────────────────┬───────┘
18
+ │ │
19
+ ┌──────▼──────┐ ┌─────────▼───────┐
20
+ │ Local DB │◄───►│ Remote API │
21
+ │ (primary) │ │ (sync only) │
22
+ └─────────────┘ └─────────────────┘
23
+ ```
24
+
25
+ **Rule:** UI always reads local. API is sync-only, never primary.
26
+
27
+ ---
28
+
29
+ ## React Native — WatermelonDB (Reference Implementation)
30
+
31
+ ### Schema
32
+
33
+ ```typescript
34
+ // db/schema.ts
35
+ export const schema = appSchema({
36
+ version: 1,
37
+ tables: [
38
+ tableSchema({
39
+ name: 'posts',
40
+ columns: [
41
+ { name: 'title', type: 'string' },
42
+ { name: 'body', type: 'string' },
43
+ { name: 'author_id', type: 'string' },
44
+ { name: 'created_at', type: 'number' },
45
+ { name: 'updated_at', type: 'number' },
46
+ { name: 'is_synced', type: 'boolean' },
47
+ { name: 'is_deleted', type: 'boolean' }, // soft delete required for sync
48
+ ],
49
+ }),
50
+ ],
51
+ });
52
+ ```
53
+
54
+ ### Model
55
+
56
+ ```typescript
57
+ export default class Post extends Model {
58
+ static table = 'posts';
59
+ static associations = {
60
+ comments: { type: 'has_many', foreignKey: 'post_id' },
61
+ };
62
+
63
+ @field('title') title!: string;
64
+ @field('body') body!: string;
65
+ @field('author_id') authorId!: string;
66
+ @readonly @date('created_at') createdAt!: Date;
67
+ @field('updated_at') updatedAt!: number;
68
+ @field('is_synced') isSynced!: boolean;
69
+ @field('is_deleted') isDeleted!: boolean;
70
+
71
+ async markAsDeleted() {
72
+ await this.update(post => {
73
+ post.isDeleted = true;
74
+ post.isSynced = false;
75
+ });
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### Sync Engine
81
+
82
+ ```typescript
83
+ export async function syncDatabase() {
84
+ await synchronize({
85
+ database,
86
+ pullChanges: async ({ lastPulledAt }) => {
87
+ const { changes, timestamp } = await api.get('/sync', {
88
+ params: { last_pulled_at: lastPulledAt },
89
+ });
90
+ return { changes, timestamp };
91
+ },
92
+ pushChanges: async ({ changes, lastPulledAt }) => {
93
+ await api.post('/sync', { changes, last_pulled_at: lastPulledAt });
94
+ },
95
+ migrationsEnabledAtVersion: 1,
96
+ });
97
+ }
98
+
99
+ export function startBackgroundSync() {
100
+ const interval = setInterval(syncDatabase, 30000);
101
+ AppState.addEventListener('change', state => {
102
+ if (state === 'active') syncDatabase();
103
+ });
104
+ return () => clearInterval(interval);
105
+ }
106
+ ```
107
+
108
+ ### Repository
109
+
110
+ ```typescript
111
+ export class PostRepository {
112
+ private posts = database.get<Post>('posts');
113
+
114
+ getAll() {
115
+ return this.posts.query(
116
+ Q.where('is_deleted', false),
117
+ Q.sortBy('created_at', Q.desc),
118
+ ).fetch();
119
+ }
120
+
121
+ observeAll() {
122
+ return this.posts.query(
123
+ Q.where('is_deleted', false),
124
+ Q.sortBy('created_at', Q.desc),
125
+ ).observe();
126
+ }
127
+
128
+ async create(data: { title: string; body: string; authorId: string }) {
129
+ return database.write(async () => {
130
+ return this.posts.create(post => {
131
+ Object.assign(post, data);
132
+ post.isSynced = false;
133
+ post.isDeleted = false;
134
+ post.updatedAt = Date.now();
135
+ });
136
+ });
137
+ }
138
+
139
+ async update(post: Post, data: Partial<{ title: string; body: string }>) {
140
+ return database.write(async () =>
141
+ post.update(p => { Object.assign(p, data); p.isSynced = false; p.updatedAt = Date.now(); })
142
+ );
143
+ }
144
+
145
+ async delete(post: Post) {
146
+ return database.write(async () => post.markAsDeleted());
147
+ }
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Flutter — Drift + Riverpod
154
+
155
+ ### Key differences from RN
156
+
157
+ ```dart
158
+ // Table definition
159
+ class Posts extends Table {
160
+ TextColumn get id => text()();
161
+ TextColumn get title => text()();
162
+ BoolColumn get isSynced => boolean().withDefault(const Constant(false))();
163
+ BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
164
+ @override Set<Column> get primaryKey => {id};
165
+ }
166
+
167
+ // Watch (reactive, like observeAll)
168
+ Stream<List<Post>> watchAllPosts() => (select(posts)
169
+ ..where((p) => p.isDeleted.equals(false))
170
+ ..orderBy([(p) => Ordering.desc(p.createdAt)]))
171
+ .watch();
172
+
173
+ // Soft delete
174
+ Future<void> softDeletePost(String id) => (update(posts)).write(
175
+ PostsCompanion(id: Value(id), isDeleted: const Value(true), isSynced: const Value(false)),
176
+ );
177
+ ```
178
+
179
+ ```dart
180
+ // Sync: check connectivity first
181
+ Future<void> sync() async {
182
+ final result = await Connectivity().checkConnectivity();
183
+ if (result == ConnectivityResult.none) return;
184
+ await _pushChanges();
185
+ await _pullChanges();
186
+ }
187
+
188
+ // Background sync on connectivity restore
189
+ Connectivity().onConnectivityChanged.listen((result) {
190
+ if (result != ConnectivityResult.none) sync();
191
+ });
192
+ ```
193
+
194
+ ---
195
+
196
+ ## iOS — SwiftData (iOS 17+) / Core Data
197
+
198
+ ```swift
199
+ @Model final class Post {
200
+ @Attribute(.unique) var id: String
201
+ var title: String
202
+ var isSynced: Bool = false
203
+ var isDeleted: Bool = false
204
+ var updatedAt: Date?
205
+ }
206
+
207
+ // Sync with NWPathMonitor
208
+ private func startNetworkMonitoring() {
209
+ monitor.pathUpdateHandler = { [weak self] path in
210
+ Task { @MainActor in
211
+ if path.status == .satisfied { await self?.sync() }
212
+ }
213
+ }
214
+ monitor.start(queue: queue)
215
+ }
216
+
217
+ func sync() async {
218
+ guard isConnected else { return }
219
+ let unsynced = try await repository.fetchUnsynced()
220
+ for post in unsynced {
221
+ post.isDeleted
222
+ ? try await apiClient.deletePost(post.id)
223
+ : try await apiClient.upsertPost(post)
224
+ post.isSynced = true
225
+ }
226
+ let remote = try await apiClient.fetchPosts()
227
+ for post in remote { try await repository.upsert(post) }
228
+ }
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Android — Room + WorkManager
234
+
235
+ ```kotlin
236
+ @Entity(tableName = "posts")
237
+ data class PostEntity(
238
+ @PrimaryKey val id: String,
239
+ val title: String,
240
+ val isSynced: Boolean = false,
241
+ val isDeleted: Boolean = false,
242
+ val updatedAt: Long? = null,
243
+ )
244
+
245
+ @Dao interface PostDao {
246
+ @Query("SELECT * FROM posts WHERE isDeleted = 0 ORDER BY createdAt DESC")
247
+ fun observeAll(): Flow<List<PostEntity>>
248
+
249
+ @Query("SELECT * FROM posts WHERE isSynced = 0")
250
+ suspend fun getUnsynced(): List<PostEntity>
251
+
252
+ @Query("UPDATE posts SET isDeleted = 1, isSynced = 0 WHERE id = :id")
253
+ suspend fun softDelete(id: String)
254
+ }
255
+
256
+ // Use WorkManager for background sync (respects battery/network constraints)
257
+ fun triggerSync() {
258
+ val request = OneTimeWorkRequestBuilder<SyncWorker>()
259
+ .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
260
+ .build()
261
+ WorkManager.getInstance(context).enqueue(request)
262
+ }
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Conflict Resolution
268
+
269
+ ```typescript
270
+ // 1. Last Write Wins (simple, most apps)
271
+ const resolve = (local: Post, remote: Post) =>
272
+ local.updatedAt > remote.updatedAt ? local : remote;
273
+
274
+ // 2. Field-Level Merge (collaborative editing)
275
+ const merge = (local: Post, remote: Post, base: Post) => ({
276
+ ...base,
277
+ title: local.title !== base.title ? local.title : remote.title,
278
+ body: local.body !== base.body ? local.body : remote.body,
279
+ updatedAt: Date.now(),
280
+ });
281
+
282
+ // 3. Operational Transform (counters/accumulators)
283
+ const resolveCounter = (local: Counter, remote: Counter, base: Counter) => ({
284
+ ...remote,
285
+ value: base.value + (local.value - base.value) + (remote.value - base.value),
286
+ });
287
+ ```
288
+
289
+ ---
290
+
291
+ ## UI Components
292
+
293
+ ```typescript
294
+ // Offline banner
295
+ export function OfflineBanner() {
296
+ const [isOffline, setIsOffline] = useState(false);
297
+ useEffect(() => NetInfo.addEventListener(s => setIsOffline(!s.isConnected)), []);
298
+ if (!isOffline) return null;
299
+ return <Banner message="Offline — changes sync when connected." />;
300
+ }
301
+
302
+ // Sync status
303
+ type SyncStatus = 'synced' | 'pending' | 'syncing' | 'error';
304
+ const STATUS_CONFIG = {
305
+ synced: { icon: 'check-circle', color: 'green' },
306
+ pending: { icon: 'clock', color: 'orange' },
307
+ syncing: { icon: 'sync', color: 'blue' },
308
+ error: { icon: 'alert-circle', color: 'red' },
309
+ };
310
+
311
+ // Optimistic update with rollback
312
+ const updateOptimistic = async (id: string, updates: Partial<Post>) => {
313
+ const prev = queryClient.getQueryData(['posts', id]);
314
+ queryClient.setQueryData(['posts', id], old => ({ ...old, ...updates }));
315
+ try {
316
+ await repository.update(id, updates);
317
+ await syncDatabase();
318
+ } catch {
319
+ queryClient.setQueryData(['posts', id], prev);
320
+ }
321
+ };
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Database Selection
327
+
328
+ | DB | Platform | Use when |
329
+ |----|----------|----------|
330
+ | **WatermelonDB** | React Native | Complex queries, observables, built-in sync |
331
+ | **MMKV** | React Native | Key-value only, speed critical |
332
+ | **Realm** | RN / Flutter / iOS | Cross-platform, reactive |
333
+ | **SQLite** | All | Full SQL control |
334
+ | **Drift** | Flutter | Type-safe, migrations, code gen |
335
+ | **SwiftData** | iOS 17+ | Simple models, native |
336
+ | **Core Data** | iOS | Complex relationships, migrations |
337
+ | **Room** | Android | Flow/LiveData integration |
338
+
339
+ ---
340
+
341
+ ## Checklist
342
+
343
+ ```
344
+ Data:
345
+ □ All data written to local DB first
346
+ □ Soft deletes (never hard delete)
347
+ □ isSynced flag per record
348
+ □ Conflict resolution strategy defined
349
+ □ Retry for failed syncs
350
+
351
+ UI:
352
+ □ Offline banner
353
+ □ Sync status indicator
354
+ □ Optimistic updates
355
+ □ Pull-to-refresh triggers sync
356
+
357
+ Sync:
358
+ □ Sync on app foreground
359
+ □ Sync on connectivity restore
360
+ □ Background sync (30s interval)
361
+ □ Sync doesn't block UI thread
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Anti-Patterns
367
+
368
+ ```
369
+ ❌ UI reads directly from API
370
+ ❌ Blocking UI during sync
371
+ ❌ Hard deletes (breaks sync)
372
+ ❌ Syncing on every keystroke
373
+ ❌ No retry for failed syncs
374
+ ❌ No offline indicator
375
+ ❌ Assuming network is available
376
+ ❌ Losing data on conflict
377
+ ```
@@ -0,0 +1,210 @@
1
+ # Performance Prediction — Simulate Before Deploy
2
+
3
+ > Calculate frame rates, bridge calls, and memory before deploying code.
4
+
5
+ ## Performance Prophet Pattern
6
+
7
+ **Predict behavior BEFORE deployment** using mathematical models.
8
+
9
+ ### Frame Budget Calculation
10
+
11
+ ```
12
+ TARGET: 60 FPS = 16.67ms per frame
13
+ PROMOTION: 120 FPS = 8.33ms per frame
14
+
15
+ BUDGET BREAKDOWN (React Native):
16
+ - JavaScript execution: 8ms
17
+ - Bridge calls: 3ms
18
+ - Native rendering: 4ms
19
+ - Layout: 1.67ms
20
+
21
+ RULE: Total < 16.67ms for 60 FPS
22
+ ```
23
+
24
+ ### Predict List Performance
25
+
26
+ ```typescript
27
+ // Given code:
28
+ <FlatList
29
+ data={items} // 50 items
30
+ renderItem={({ item }) => (
31
+ <Item
32
+ title={item.title} // 1 bridge call
33
+ image={item.image} // 1 bridge call
34
+ onPress={() => logEvent(item.id)} // 1 bridge call
35
+ />
36
+ )}
37
+ />
38
+
39
+ // Calculate:
40
+ Bridge calls per item: 3
41
+ Total items: 50
42
+ Total bridge calls: 50 × 3 = 150 calls
43
+ Time per call: ~0.3ms
44
+ Total time: 150 × 0.3ms = 45ms per frame
45
+
46
+ // Prediction:
47
+ 16.67ms (60 FPS budget) vs 45ms (actual)
48
+ Result: 16.67 / 45 = 0.37 → 37% of 60 FPS = 22 FPS
49
+ Verdict: ❌ JANK - Users will notice lag
50
+
51
+ // Auto-fix suggestions:
52
+ 1. Use getItemLayout (saves layout calculations)
53
+ 2. Memoize renderItem component
54
+ 3. Defer logEvent to InteractionManager
55
+ 4. Use native driver for animations
56
+ 5. Implement virtualization (windowSize: 5)
57
+
58
+ Expected after fix: ~12ms per frame → 60 FPS ✓
59
+ ```
60
+
61
+ ### Predict Memory Usage
62
+
63
+ ```
64
+ IMAGE CALCULATION:
65
+ - Image: 1000×1000 pixels
66
+ - Color depth: 4 bytes (RGBA)
67
+ - Memory: 1000 × 1000 × 4 = 4MB per image
68
+
69
+ List with 50 images:
70
+ - No optimization: 50 × 4MB = 200MB
71
+ - With thumbnail (200×200): 50 × 0.16MB = 8MB
72
+ - Verdict: Use thumbnails + lazy load
73
+
74
+ MEMORY BUDGET:
75
+ - iOS: ~120MB baseline
76
+ - Android: ~80MB baseline (varies by device)
77
+ - Target: < 150MB total for stability
78
+ ```
79
+
80
+ ### Predict Bundle Impact
81
+
82
+ ```
83
+ NEW PACKAGE: moment.js
84
+ Size: 230KB minified
85
+ Bundle before: 2.1MB
86
+ Bundle after: 2.33MB
87
+ Impact: +11% bundle size
88
+
89
+ Alternatives:
90
+ - date-fns/esm: 50KB (modular)
91
+ - day.js: 2KB (minimal)
92
+ Recommendation: Use day.js → 98% size reduction
93
+ ```
94
+
95
+ ## Quick Calculations
96
+
97
+ ### 1. FlatList Frame Rate
98
+ ```
99
+ Formula: FPS = 16.67ms / (bridge_calls × 0.3ms + render_time)
100
+
101
+ Example:
102
+ - 100 items
103
+ - 5 bridge calls per item
104
+ - 2ms render time per item
105
+
106
+ Time per frame: (100 × 5 × 0.3ms) + (100 × 2ms) = 150ms + 200ms = 350ms
107
+ FPS: 16.67 / 350 = 0.048 → 4.8 FPS
108
+
109
+ Verdict: ❌ UNUSABLE - Must optimize
110
+ ```
111
+
112
+ ### 2. Animation Smoothness
113
+ ```
114
+ Rule: Use native driver when possible
115
+
116
+ With native driver:
117
+ - Runs at 60 FPS (or 120 FPS ProMotion)
118
+ - No bridge calls per frame
119
+ - Smooth animations
120
+
121
+ Without native driver:
122
+ - JavaScript thread: ~16.67ms budget
123
+ - Each frame: ~2-3 bridge calls
124
+ - Often drops to 30-45 FPS
125
+
126
+ Verdict: Always use { useNativeDriver: true }
127
+ ```
128
+
129
+ ### 3. Network Overhead
130
+ ```
131
+ API Response: 500KB JSON
132
+ Parse time: ~50ms (varies by device)
133
+ UI block: 50ms = 3 frames dropped
134
+
135
+ Optimization:
136
+ - Paginate: 50KB per page → 5ms parse
137
+ - Background thread: 0 frames dropped
138
+ ```
139
+
140
+ ## Platform-Specific Predictions
141
+
142
+ ### React Native
143
+ ```
144
+ KNOWN BOTTLENECKS:
145
+ 1. Bridge calls: 0.3ms each
146
+ 2. console.log: 10-50ms (dev mode)
147
+ 3. Inline styles: Re-creates every render
148
+ 4. Anonymous functions in render: Creates new reference
149
+
150
+ OPTIMIZATION PRIORITY:
151
+ 1. Remove console.log (50ms → 0ms)
152
+ 2. StyleSheet.create (5ms → 0.1ms)
153
+ 3. useCallback/useMemo (prevents re-renders)
154
+ 4. Native driver animations (60 FPS guaranteed)
155
+ ```
156
+
157
+ ### Flutter
158
+ ```
159
+ KNOWN BOTTLENECKS:
160
+ 1. Build method: Should be < 8ms
161
+ 2. setState: Triggers full rebuild
162
+ 3. Large widget trees: O(n) complexity
163
+
164
+ OPTIMIZATION PRIORITY:
165
+ 1. const constructors (immutable widgets)
166
+ 2. ListView.builder vs ListView (O(visible) vs O(n))
167
+ 3. RepaintBoundary (isolate repaints)
168
+ 4. Selective rebuilds (Consumer vs rebuild all)
169
+ ```
170
+
171
+ ## Prediction Workflow
172
+
173
+ ```
174
+ BEFORE IMPLEMENTING:
175
+ 1. Estimate bridge calls (RN) or build time (Flutter)
176
+ 2. Calculate frame budget impact
177
+ 3. Predict FPS: 16.67ms / total_time
178
+ 4. If < 60 FPS: redesign or optimize first
179
+
180
+ AFTER IMPLEMENTING:
181
+ 1. Profile with React DevTools / Flutter DevTools
182
+ 2. Compare predicted vs actual
183
+ 3. Adjust model if off by > 20%
184
+ 4. Document learnings for next time
185
+ ```
186
+
187
+ ## Auto-Suggestions
188
+
189
+ ```
190
+ If prediction shows < 60 FPS:
191
+
192
+ FOR LISTS:
193
+ ✓ Use getItemLayout
194
+ ✓ Memoize renderItem
195
+ ✓ Reduce bridge calls
196
+ ✓ Virtualization (windowSize)
197
+ ✓ Thumbnail images
198
+
199
+ FOR ANIMATIONS:
200
+ ✓ Use native driver
201
+ ✓ Avoid layout animations
202
+ ✓ Prefer transform/opacity
203
+ ✓ Use InteractionManager
204
+
205
+ FOR MEMORY:
206
+ ✓ Image optimization
207
+ ✓ Lazy loading
208
+ ✓ Pagination
209
+ ✓ Clear caches
210
+ ```