@buivietphi/skill-mobile-mt 1.4.0 → 1.4.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.
Potentially problematic release.
This version of @buivietphi/skill-mobile-mt might be problematic. Click here for more details.
- package/AGENTS.md +10 -9
- package/README.md +6 -5
- package/SKILL.md +6 -3
- package/bin/install.mjs +1 -1
- package/package.json +1 -1
- package/shared/ci-cd.md +423 -0
- package/shared/testing-strategy.md +332 -0
- package/shared/on-device-ai.md +0 -175
package/AGENTS.md
CHANGED
|
@@ -62,7 +62,8 @@ skill-mobile-mt/
|
|
|
62
62
|
├── release-checklist.md ← App Store/Play Store checklist (587 tokens)
|
|
63
63
|
│
|
|
64
64
|
├── offline-first.md ← Local-first + sync patterns (2,566 tokens)
|
|
65
|
-
├──
|
|
65
|
+
├── testing-strategy.md ← Detox + Maestro + XCUITest + Espresso E2E (2,200 tokens)
|
|
66
|
+
├── ci-cd.md ← GitHub Actions CI templates (2,500 tokens)
|
|
66
67
|
│
|
|
67
68
|
├── ── TEMPLATES (copy to your project) ────────────────────
|
|
68
69
|
├── claude-md-template.md ← CLAUDE.md for Claude Code (copy to project root)
|
|
@@ -71,7 +72,7 @@ skill-mobile-mt/
|
|
|
71
72
|
|
|
72
73
|
**Token totals:**
|
|
73
74
|
- Smart load (1 platform + core shared): **~38,600 tokens** (30.2% of 128K)
|
|
74
|
-
- Full load (all files): **~
|
|
75
|
+
- Full load (all files): **~74,700 tokens** (58.4% of 128K)
|
|
75
76
|
|
|
76
77
|
---
|
|
77
78
|
|
|
@@ -110,7 +111,8 @@ The agent reads the task, then decides which extra file to load:
|
|
|
110
111
|
| "Install this package / upgrade SDK" | `shared/version-management.md` |
|
|
111
112
|
| "Prepare for App Store / Play Store" | `shared/release-checklist.md` |
|
|
112
113
|
| "Weird issue, not sure why" | `shared/common-pitfalls.md` |
|
|
113
|
-
| "
|
|
114
|
+
| "Write / run E2E tests" | `shared/testing-strategy.md` |
|
|
115
|
+
| "Setup CI/CD / GitHub Actions" | `shared/ci-cd.md` |
|
|
114
116
|
|
|
115
117
|
**Load cost:** +500 to +3,500 tokens per on-demand file.
|
|
116
118
|
|
|
@@ -145,7 +147,7 @@ The agent reads the task, then decides which extra file to load:
|
|
|
145
147
|
```yaml
|
|
146
148
|
skill:
|
|
147
149
|
name: skill-mobile-mt
|
|
148
|
-
version: "1.4.
|
|
150
|
+
version: "1.4.2"
|
|
149
151
|
author: buivietphi
|
|
150
152
|
category: engineering
|
|
151
153
|
tags:
|
|
@@ -178,7 +180,6 @@ skill:
|
|
|
178
180
|
# - shared/document-analysis.md
|
|
179
181
|
# - shared/release-checklist.md
|
|
180
182
|
# - shared/common-pitfalls.md
|
|
181
|
-
# - shared/on-device-ai.md
|
|
182
183
|
|
|
183
184
|
project:
|
|
184
185
|
description: "Read current project, adapt to its framework and conventions"
|
|
@@ -213,9 +214,9 @@ skill:
|
|
|
213
214
|
java: ".java files in app/src/"
|
|
214
215
|
|
|
215
216
|
context_budget:
|
|
216
|
-
max_tokens:
|
|
217
|
+
max_tokens: 74700
|
|
217
218
|
smart_load_tokens: 38600
|
|
218
|
-
savings: "~
|
|
219
|
+
savings: "~48%"
|
|
219
220
|
```
|
|
220
221
|
|
|
221
222
|
---
|
|
@@ -261,7 +262,6 @@ Every agent MUST follow this loading sequence:
|
|
|
261
262
|
- shared/observability.md (when adding logging, analytics, crash tracking)
|
|
262
263
|
- shared/common-pitfalls.md (when encountering unfamiliar errors)
|
|
263
264
|
- shared/release-checklist.md (when preparing for App Store/Play Store submission)
|
|
264
|
-
- shared/on-device-ai.md (when adding Core ML / TFLite / on-device inference)
|
|
265
265
|
|
|
266
266
|
7. SKIP non-matching platform subfolders (saves ~66% context)
|
|
267
267
|
```
|
|
@@ -283,7 +283,8 @@ Priority 6 (ON-DEMAND): shared/observability.md — Sessions as 4th pillar
|
|
|
283
283
|
Priority 6 (ON-DEMAND): shared/document-analysis.md — Parse images/PDFs → code
|
|
284
284
|
Priority 6 (ON-DEMAND): shared/release-checklist.md — Pre-release verification
|
|
285
285
|
Priority 6 (ON-DEMAND): shared/common-pitfalls.md — Known issue patterns
|
|
286
|
-
Priority 6 (ON-DEMAND): shared/
|
|
286
|
+
Priority 6 (ON-DEMAND): shared/testing-strategy.md — Detox + Maestro + XCUITest + Espresso E2E
|
|
287
|
+
Priority 6 (ON-DEMAND): shared/ci-cd.md — GitHub Actions CI/CD templates
|
|
287
288
|
```
|
|
288
289
|
|
|
289
290
|
---
|
package/README.md
CHANGED
|
@@ -232,7 +232,7 @@ iOS only?
|
|
|
232
232
|
| SKILL.md only | ~13,200 | 10.3% | 6.6% |
|
|
233
233
|
| + 1 platform + core shared/ | ~38,600 | 30.2% | 19.3% |
|
|
234
234
|
| Cross-platform (RN/Flutter + iOS + Android) | ~53,000 | 41.4% | 26.5% |
|
|
235
|
-
| All files loaded | ~
|
|
235
|
+
| All files loaded | ~74,700 | 58.4% | 37.4% |
|
|
236
236
|
| **Smart load (recommended)** | **~38,600** | **30.2%** | **19.3%** |
|
|
237
237
|
|
|
238
238
|
### Per-file token breakdown
|
|
@@ -259,10 +259,11 @@ iOS only?
|
|
|
259
259
|
| `shared/version-management.md` | 3,500 |
|
|
260
260
|
| `shared/observability.md` | 3,000 |
|
|
261
261
|
| `shared/offline-first.md` | 2,566 |
|
|
262
|
-
| `shared/
|
|
262
|
+
| `shared/testing-strategy.md` | 2,200 |
|
|
263
|
+
| `shared/ci-cd.md` | 2,500 |
|
|
263
264
|
| `shared/claude-md-template.md` | ~500 |
|
|
264
265
|
| `shared/agent-rules-template.md` | ~2,500 |
|
|
265
|
-
| **Total** | **~
|
|
266
|
+
| **Total** | **~53,500** |
|
|
266
267
|
|
|
267
268
|
## Installed Structure
|
|
268
269
|
|
|
@@ -294,7 +295,8 @@ iOS only?
|
|
|
294
295
|
├── observability.md Sessions as 4th pillar
|
|
295
296
|
├── release-checklist.md Pre-release verification
|
|
296
297
|
├── offline-first.md Local-first + sync patterns
|
|
297
|
-
├──
|
|
298
|
+
├── testing-strategy.md Detox + Maestro + XCUITest + Espresso E2E
|
|
299
|
+
├── ci-cd.md GitHub Actions CI/CD templates
|
|
298
300
|
├── claude-md-template.md CLAUDE.md template for projects
|
|
299
301
|
└── agent-rules-template.md Rules templates for all agents
|
|
300
302
|
```
|
|
@@ -390,7 +392,6 @@ your-project/
|
|
|
390
392
|
- **Anti-Pattern Detection** (`anti-patterns.md`): Detect PII leaks (CRITICAL), high cardinality tags, unbounded payloads, unstructured logs, sync telemetry on main thread — with auto-fix suggestions
|
|
391
393
|
- **Performance Prediction** (`performance-prediction.md`): Calculate frame budget, FlatList bridge calls, and memory usage BEFORE writing code. Example: `50 items × 3 bridge calls × 0.3ms = 45ms/frame → 22 FPS ❌ JANK`
|
|
392
394
|
- **Platform Excellence** (`platform-excellence.md`): iOS 18+ vs Android 15+ native UX standards — navigation patterns, typography, haptic feedback types, permission timing, ratings prompt flow, Live Activities/Dynamic Island, performance targets (cold start < 1s iOS, < 1.5s Android)
|
|
393
|
-
- **On-Device AI** (`on-device-ai.md`): Decision matrix (API vs on-device), Core ML (iOS), ML Kit + MediaPipe (Android), llama.cpp cross-platform, TFLite Flutter, React Native ML Kit — with performance rules and model size guidance
|
|
394
395
|
- **Version Management** (`version-management.md`): Full SDK compatibility matrix for RN 0.73-0.76, Expo 50-52, Flutter 3.22-3.27, iOS 16-18, Android 13-15. Check SDK compat BEFORE `npm install`. Release-mode testing protocol.
|
|
395
396
|
- **Observability** (`observability.md`): Sessions as the 4th pillar (Metrics + Logs + Traces + **Sessions**). Session lifecycle, enrichment API, unified instrumentation stack, correlation queries. Every event carries `session_id` for full user journey reconstruction.
|
|
396
397
|
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-mobile-mt
|
|
3
3
|
description: "Master Senior Mobile Engineer. Patterns from 30+ production repos (200k+ GitHub stars: Ignite, Expensify, Mattermost, Immich, AppFlowy, Now in Android, TCA). Use when: building mobile features, fixing mobile bugs, reviewing mobile code, mobile architecture, React Native, Flutter, iOS Swift, Android Kotlin, mobile performance, mobile security audit, mobile code review, app release. Two modes: (1) default = pre-built production patterns, (2) 'project' = reads current project and adapts."
|
|
4
|
-
version: "1.4.
|
|
4
|
+
version: "1.4.2"
|
|
5
5
|
author: buivietphi
|
|
6
6
|
priority: high
|
|
7
7
|
user-invocable: true
|
|
@@ -130,8 +130,11 @@ USER REQUEST → ACTION (Read tool required)
|
|
|
130
130
|
"Offline / cache / sync" → Read: shared/offline-first.md
|
|
131
131
|
then: implement local-first architecture
|
|
132
132
|
|
|
133
|
-
"
|
|
134
|
-
then:
|
|
133
|
+
"Write/run E2E tests" → Read: shared/testing-strategy.md
|
|
134
|
+
then: Detox (RN) or Maestro (cross-platform) or XCUITest/Espresso
|
|
135
|
+
|
|
136
|
+
"Setup CI/CD / GitHub Actions" → Read: shared/ci-cd.md
|
|
137
|
+
then: test → build → distribute pipeline
|
|
135
138
|
|
|
136
139
|
```
|
|
137
140
|
|
package/bin/install.mjs
CHANGED
|
@@ -70,7 +70,7 @@ const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
|
|
|
70
70
|
|
|
71
71
|
function banner() {
|
|
72
72
|
log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────────┐`);
|
|
73
|
-
log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.
|
|
73
|
+
log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.2 │`);
|
|
74
74
|
log(` │ Master Senior Mobile Engineer │`);
|
|
75
75
|
log(` │ │`);
|
|
76
76
|
log(` │ Claude · Cline · Roo Code · Cursor · Windsurf │`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buivietphi/skill-mobile-mt",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Master Senior Mobile Engineer skill for AI agents. Pre-built patterns from 18 production apps + local project adaptation. React Native, Flutter, iOS, Android. Supports Claude, Gemini, Kimi, Cursor, Copilot, Antigravity.",
|
|
5
5
|
"author": "buivietphi",
|
|
6
6
|
"license": "MIT",
|
package/shared/ci-cd.md
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# CI/CD Pipelines — GitHub Actions for Mobile
|
|
2
|
+
|
|
3
|
+
> Automate: test → build → distribute. Never ship without CI.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## React Native — CI Pipeline
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
# .github/workflows/rn-ci.yml
|
|
11
|
+
name: React Native CI
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches: [main, develop]
|
|
16
|
+
pull_request:
|
|
17
|
+
branches: [main]
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: '20'
|
|
28
|
+
cache: 'yarn' # or npm, pnpm
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: yarn install --frozen-lockfile
|
|
32
|
+
|
|
33
|
+
- name: TypeScript check
|
|
34
|
+
run: yarn tsc --noEmit
|
|
35
|
+
|
|
36
|
+
- name: Lint
|
|
37
|
+
run: yarn lint
|
|
38
|
+
|
|
39
|
+
- name: Unit tests
|
|
40
|
+
run: yarn test --ci --coverage --maxWorkers=2
|
|
41
|
+
|
|
42
|
+
- name: Upload coverage
|
|
43
|
+
uses: codecov/codecov-action@v4
|
|
44
|
+
with:
|
|
45
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
46
|
+
|
|
47
|
+
build-android:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
needs: test
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
|
|
53
|
+
- uses: actions/setup-node@v4
|
|
54
|
+
with:
|
|
55
|
+
node-version: '20'
|
|
56
|
+
cache: 'yarn'
|
|
57
|
+
|
|
58
|
+
- uses: actions/setup-java@v4
|
|
59
|
+
with:
|
|
60
|
+
distribution: 'temurin'
|
|
61
|
+
java-version: '17'
|
|
62
|
+
|
|
63
|
+
- name: Cache Gradle
|
|
64
|
+
uses: actions/cache@v4
|
|
65
|
+
with:
|
|
66
|
+
path: |
|
|
67
|
+
~/.gradle/caches
|
|
68
|
+
~/.gradle/wrapper
|
|
69
|
+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
|
70
|
+
|
|
71
|
+
- name: Install dependencies
|
|
72
|
+
run: yarn install --frozen-lockfile
|
|
73
|
+
|
|
74
|
+
- name: Build Android APK (debug)
|
|
75
|
+
run: cd android && ./gradlew assembleDebug
|
|
76
|
+
|
|
77
|
+
- name: Upload APK
|
|
78
|
+
uses: actions/upload-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: app-debug.apk
|
|
81
|
+
path: android/app/build/outputs/apk/debug/app-debug.apk
|
|
82
|
+
|
|
83
|
+
build-ios:
|
|
84
|
+
runs-on: macos-15
|
|
85
|
+
needs: test
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v4
|
|
88
|
+
|
|
89
|
+
- uses: actions/setup-node@v4
|
|
90
|
+
with:
|
|
91
|
+
node-version: '20'
|
|
92
|
+
cache: 'yarn'
|
|
93
|
+
|
|
94
|
+
- name: Cache CocoaPods
|
|
95
|
+
uses: actions/cache@v4
|
|
96
|
+
with:
|
|
97
|
+
path: ios/Pods
|
|
98
|
+
key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
|
|
99
|
+
|
|
100
|
+
- name: Install dependencies
|
|
101
|
+
run: yarn install --frozen-lockfile && cd ios && pod install
|
|
102
|
+
|
|
103
|
+
- name: Build iOS (simulator)
|
|
104
|
+
run: |
|
|
105
|
+
xcodebuild -workspace ios/MyApp.xcworkspace \
|
|
106
|
+
-scheme MyApp \
|
|
107
|
+
-sdk iphonesimulator \
|
|
108
|
+
-configuration Debug \
|
|
109
|
+
-derivedDataPath ios/build \
|
|
110
|
+
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## React Native — E2E with Detox
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
# .github/workflows/rn-e2e.yml
|
|
119
|
+
name: E2E Tests (Detox)
|
|
120
|
+
|
|
121
|
+
on:
|
|
122
|
+
push:
|
|
123
|
+
branches: [main]
|
|
124
|
+
|
|
125
|
+
jobs:
|
|
126
|
+
e2e-ios:
|
|
127
|
+
runs-on: macos-15
|
|
128
|
+
steps:
|
|
129
|
+
- uses: actions/checkout@v4
|
|
130
|
+
|
|
131
|
+
- uses: actions/setup-node@v4
|
|
132
|
+
with:
|
|
133
|
+
node-version: '20'
|
|
134
|
+
cache: 'yarn'
|
|
135
|
+
|
|
136
|
+
- name: Install dependencies
|
|
137
|
+
run: yarn install --frozen-lockfile && cd ios && pod install
|
|
138
|
+
|
|
139
|
+
- name: Install Detox CLI
|
|
140
|
+
run: npm install -g detox-cli
|
|
141
|
+
|
|
142
|
+
- name: Build for Detox
|
|
143
|
+
run: detox build --configuration ios.sim.debug
|
|
144
|
+
|
|
145
|
+
- name: Run E2E tests
|
|
146
|
+
run: detox test --configuration ios.sim.debug --headless
|
|
147
|
+
|
|
148
|
+
- name: Upload Detox artifacts on failure
|
|
149
|
+
if: failure()
|
|
150
|
+
uses: actions/upload-artifact@v4
|
|
151
|
+
with:
|
|
152
|
+
name: detox-artifacts
|
|
153
|
+
path: artifacts/
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Flutter — CI Pipeline
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
# .github/workflows/flutter-ci.yml
|
|
162
|
+
name: Flutter CI
|
|
163
|
+
|
|
164
|
+
on:
|
|
165
|
+
push:
|
|
166
|
+
branches: [main, develop]
|
|
167
|
+
pull_request:
|
|
168
|
+
branches: [main]
|
|
169
|
+
|
|
170
|
+
jobs:
|
|
171
|
+
test:
|
|
172
|
+
runs-on: ubuntu-latest
|
|
173
|
+
steps:
|
|
174
|
+
- uses: actions/checkout@v4
|
|
175
|
+
|
|
176
|
+
- uses: subosito/flutter-action@v2
|
|
177
|
+
with:
|
|
178
|
+
flutter-version: '3.27.x'
|
|
179
|
+
cache: true
|
|
180
|
+
|
|
181
|
+
- name: Install dependencies
|
|
182
|
+
run: flutter pub get
|
|
183
|
+
|
|
184
|
+
- name: Analyze
|
|
185
|
+
run: flutter analyze
|
|
186
|
+
|
|
187
|
+
- name: Unit + Widget tests
|
|
188
|
+
run: flutter test --coverage
|
|
189
|
+
|
|
190
|
+
- name: Upload coverage
|
|
191
|
+
uses: codecov/codecov-action@v4
|
|
192
|
+
|
|
193
|
+
build-android:
|
|
194
|
+
runs-on: ubuntu-latest
|
|
195
|
+
needs: test
|
|
196
|
+
steps:
|
|
197
|
+
- uses: actions/checkout@v4
|
|
198
|
+
|
|
199
|
+
- uses: actions/setup-java@v4
|
|
200
|
+
with:
|
|
201
|
+
distribution: 'temurin'
|
|
202
|
+
java-version: '17'
|
|
203
|
+
|
|
204
|
+
- uses: subosito/flutter-action@v2
|
|
205
|
+
with:
|
|
206
|
+
flutter-version: '3.27.x'
|
|
207
|
+
cache: true
|
|
208
|
+
|
|
209
|
+
- name: Install dependencies
|
|
210
|
+
run: flutter pub get
|
|
211
|
+
|
|
212
|
+
- name: Build APK
|
|
213
|
+
run: flutter build apk --debug
|
|
214
|
+
|
|
215
|
+
- name: Upload APK
|
|
216
|
+
uses: actions/upload-artifact@v4
|
|
217
|
+
with:
|
|
218
|
+
name: flutter-debug.apk
|
|
219
|
+
path: build/app/outputs/flutter-apk/app-debug.apk
|
|
220
|
+
|
|
221
|
+
build-ios:
|
|
222
|
+
runs-on: macos-15
|
|
223
|
+
needs: test
|
|
224
|
+
steps:
|
|
225
|
+
- uses: actions/checkout@v4
|
|
226
|
+
|
|
227
|
+
- uses: subosito/flutter-action@v2
|
|
228
|
+
with:
|
|
229
|
+
flutter-version: '3.27.x'
|
|
230
|
+
cache: true
|
|
231
|
+
|
|
232
|
+
- name: Install dependencies
|
|
233
|
+
run: flutter pub get
|
|
234
|
+
|
|
235
|
+
- name: Build iOS (no codesign)
|
|
236
|
+
run: flutter build ios --debug --no-codesign
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## iOS — Release to TestFlight (Fastlane)
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
# .github/workflows/ios-release.yml
|
|
245
|
+
name: iOS Release
|
|
246
|
+
|
|
247
|
+
on:
|
|
248
|
+
push:
|
|
249
|
+
tags: ['v*']
|
|
250
|
+
|
|
251
|
+
jobs:
|
|
252
|
+
release:
|
|
253
|
+
runs-on: macos-15
|
|
254
|
+
steps:
|
|
255
|
+
- uses: actions/checkout@v4
|
|
256
|
+
|
|
257
|
+
- uses: actions/setup-node@v4
|
|
258
|
+
with:
|
|
259
|
+
node-version: '20'
|
|
260
|
+
cache: 'yarn'
|
|
261
|
+
|
|
262
|
+
- name: Install dependencies
|
|
263
|
+
run: yarn install --frozen-lockfile && cd ios && pod install
|
|
264
|
+
|
|
265
|
+
- name: Setup certificates
|
|
266
|
+
uses: apple-actions/import-codesign-certs@v2
|
|
267
|
+
with:
|
|
268
|
+
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
|
269
|
+
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
|
270
|
+
|
|
271
|
+
- name: Setup provisioning profile
|
|
272
|
+
uses: apple-actions/download-provisioning-profiles@v1
|
|
273
|
+
with:
|
|
274
|
+
bundle-id: com.myapp
|
|
275
|
+
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
|
|
276
|
+
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
|
277
|
+
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
|
|
278
|
+
|
|
279
|
+
- name: Deploy to TestFlight
|
|
280
|
+
run: bundle exec fastlane ios beta
|
|
281
|
+
env:
|
|
282
|
+
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
|
|
283
|
+
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
|
|
284
|
+
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
# ios/Fastfile
|
|
289
|
+
lane :beta do
|
|
290
|
+
build_app(
|
|
291
|
+
workspace: "MyApp.xcworkspace",
|
|
292
|
+
scheme: "MyApp",
|
|
293
|
+
configuration: "Release",
|
|
294
|
+
export_method: "app-store"
|
|
295
|
+
)
|
|
296
|
+
upload_to_testflight(skip_waiting_for_build_processing: true)
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Android — Release to Play Store (Fastlane)
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
# .github/workflows/android-release.yml
|
|
306
|
+
name: Android Release
|
|
307
|
+
|
|
308
|
+
on:
|
|
309
|
+
push:
|
|
310
|
+
tags: ['v*']
|
|
311
|
+
|
|
312
|
+
jobs:
|
|
313
|
+
release:
|
|
314
|
+
runs-on: ubuntu-latest
|
|
315
|
+
steps:
|
|
316
|
+
- uses: actions/checkout@v4
|
|
317
|
+
|
|
318
|
+
- uses: actions/setup-java@v4
|
|
319
|
+
with:
|
|
320
|
+
distribution: 'temurin'
|
|
321
|
+
java-version: '17'
|
|
322
|
+
|
|
323
|
+
- uses: actions/setup-node@v4
|
|
324
|
+
with:
|
|
325
|
+
node-version: '20'
|
|
326
|
+
cache: 'yarn'
|
|
327
|
+
|
|
328
|
+
- name: Install dependencies
|
|
329
|
+
run: yarn install --frozen-lockfile
|
|
330
|
+
|
|
331
|
+
- name: Decode keystore
|
|
332
|
+
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
|
|
333
|
+
|
|
334
|
+
- name: Build release AAB
|
|
335
|
+
run: cd android && ./gradlew bundleRelease
|
|
336
|
+
env:
|
|
337
|
+
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
|
338
|
+
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
339
|
+
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
340
|
+
|
|
341
|
+
- name: Deploy to Play Store
|
|
342
|
+
uses: r0adkll/upload-google-play@v1
|
|
343
|
+
with:
|
|
344
|
+
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
|
|
345
|
+
packageName: com.myapp
|
|
346
|
+
releaseFiles: android/app/build/outputs/bundle/release/*.aab
|
|
347
|
+
track: internal
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Required GitHub Secrets
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
# iOS
|
|
356
|
+
CERTIFICATES_P12 ← base64-encoded .p12 file
|
|
357
|
+
CERTIFICATES_P12_PASSWORD ← password for .p12
|
|
358
|
+
APPSTORE_KEY_ID ← App Store Connect API key ID
|
|
359
|
+
APPSTORE_ISSUER_ID ← App Store Connect issuer ID
|
|
360
|
+
APPSTORE_PRIVATE_KEY ← App Store Connect private key (.p8 contents)
|
|
361
|
+
|
|
362
|
+
# Android
|
|
363
|
+
KEYSTORE_BASE64 ← base64-encoded release.keystore
|
|
364
|
+
KEYSTORE_PASSWORD ← keystore password
|
|
365
|
+
KEY_ALIAS ← key alias
|
|
366
|
+
KEY_PASSWORD ← key password
|
|
367
|
+
PLAY_STORE_SERVICE_ACCOUNT_JSON ← GCP service account JSON
|
|
368
|
+
|
|
369
|
+
# Shared
|
|
370
|
+
CODECOV_TOKEN ← coverage reporting
|
|
371
|
+
MAESTRO_API_KEY ← Maestro Cloud E2E (optional)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Caching Strategy
|
|
377
|
+
|
|
378
|
+
```yaml
|
|
379
|
+
# Node modules — hash package-lock or yarn.lock
|
|
380
|
+
- uses: actions/cache@v4
|
|
381
|
+
with:
|
|
382
|
+
path: node_modules
|
|
383
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
|
384
|
+
restore-keys: ${{ runner.os }}-node-
|
|
385
|
+
|
|
386
|
+
# Gradle — hash .gradle files
|
|
387
|
+
- uses: actions/cache@v4
|
|
388
|
+
with:
|
|
389
|
+
path: |
|
|
390
|
+
~/.gradle/caches
|
|
391
|
+
~/.gradle/wrapper
|
|
392
|
+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
393
|
+
|
|
394
|
+
# CocoaPods — hash Podfile.lock
|
|
395
|
+
- uses: actions/cache@v4
|
|
396
|
+
with:
|
|
397
|
+
path: ios/Pods
|
|
398
|
+
key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
|
|
399
|
+
|
|
400
|
+
# Flutter pub — hash pubspec.lock
|
|
401
|
+
- uses: actions/cache@v4
|
|
402
|
+
with:
|
|
403
|
+
path: ~/.pub-cache
|
|
404
|
+
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Anti-Patterns
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
❌ Committing signing credentials to repo
|
|
413
|
+
❌ Running E2E on every PR (too slow — run on main only)
|
|
414
|
+
❌ No caching (3x slower builds)
|
|
415
|
+
❌ Skipping unit tests before build jobs
|
|
416
|
+
❌ Building on push to every branch
|
|
417
|
+
|
|
418
|
+
✅ Store all secrets in GitHub Secrets
|
|
419
|
+
✅ Cache node_modules + Gradle + CocoaPods + pub-cache
|
|
420
|
+
✅ Unit tests on PR, E2E on merge to main
|
|
421
|
+
✅ needs: test before build jobs
|
|
422
|
+
✅ Upload build artifacts for download/QA
|
|
423
|
+
```
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Mobile Testing Strategy — Unit + E2E
|
|
2
|
+
|
|
3
|
+
> Test the right things at the right layer. Don't test implementation details.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Testing Pyramid
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
╱ E2E ╲ ← Detox / Maestro / XCUITest / Espresso
|
|
11
|
+
╱───────╲ ← Few, slow, high-confidence
|
|
12
|
+
╱Integration╲ ← API mocking, navigation flows
|
|
13
|
+
╱─────────────╲ ← Medium count
|
|
14
|
+
╱ Unit Tests ╲ ← Jest / XCTest / JUnit
|
|
15
|
+
╱─────────────────╲ ← Many, fast, cheap
|
|
16
|
+
|
|
17
|
+
RULE: Most tests = unit. E2E = critical flows only (login, checkout, onboarding).
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Unit Tests (Jest — React Native / TypeScript)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Test hooks, not components
|
|
26
|
+
describe('useCart', () => {
|
|
27
|
+
it('adds item and updates total', () => {
|
|
28
|
+
const { result } = renderHook(() => useCart(), { wrapper: ReduxProvider })
|
|
29
|
+
act(() => { result.current.addItem(mockProduct) })
|
|
30
|
+
expect(result.current.total).toBe(mockProduct.price)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('handles addToCart API error with rollback', async () => {
|
|
34
|
+
server.use(rest.post('/cart', (req, res, ctx) => res(ctx.status(500))))
|
|
35
|
+
const { result } = renderHook(() => useCart(), { wrapper: ReduxProvider })
|
|
36
|
+
await act(async () => { await result.current.addItem(mockProduct) })
|
|
37
|
+
expect(result.current.items).toHaveLength(0) // rolled back
|
|
38
|
+
expect(result.current.error).toBeTruthy()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Test Redux slices directly
|
|
43
|
+
describe('cartSlice', () => {
|
|
44
|
+
it('sets loading state on fetchCart.pending', () => {
|
|
45
|
+
const state = cartReducer(initialState, fetchCart.pending('', undefined))
|
|
46
|
+
expect(state.status).toBe('loading')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Rules:**
|
|
52
|
+
- Test business logic (hooks, services, slices) — NOT component layout
|
|
53
|
+
- Mock API calls with `msw` (Mock Service Worker)
|
|
54
|
+
- 4 states per feature: loading / success / error / empty
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## E2E Testing — Detox (React Native)
|
|
59
|
+
|
|
60
|
+
> Best for: React Native apps, full native bridge testing.
|
|
61
|
+
|
|
62
|
+
### Setup
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Install
|
|
66
|
+
npm install --save-dev detox @config/detox
|
|
67
|
+
|
|
68
|
+
# iOS build (required before tests)
|
|
69
|
+
detox build --configuration ios.sim.debug
|
|
70
|
+
|
|
71
|
+
# Run tests
|
|
72
|
+
detox test --configuration ios.sim.debug
|
|
73
|
+
detox test --configuration android.emu.debug
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Config (`detox.config.js`)
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
module.exports = {
|
|
80
|
+
testRunner: {
|
|
81
|
+
args: { '$0': 'jest', config: 'e2e/jest.config.js' },
|
|
82
|
+
jest: { setupTimeout: 120000 }
|
|
83
|
+
},
|
|
84
|
+
apps: {
|
|
85
|
+
'ios.debug': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build' },
|
|
86
|
+
'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug' }
|
|
87
|
+
},
|
|
88
|
+
devices: {
|
|
89
|
+
simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } },
|
|
90
|
+
emulator: { type: 'android.emulator', device: { avdName: 'Pixel_7_API_34' } }
|
|
91
|
+
},
|
|
92
|
+
configurations: {
|
|
93
|
+
'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
|
|
94
|
+
'android.emu.debug': { device: 'emulator', app: 'android.debug' }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Write Detox Tests
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// e2e/login.test.ts
|
|
103
|
+
describe('Login Flow', () => {
|
|
104
|
+
beforeAll(async () => {
|
|
105
|
+
await device.launchApp({ newInstance: true })
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
beforeEach(async () => {
|
|
109
|
+
await device.reloadReactNative()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('shows error on wrong password', async () => {
|
|
113
|
+
await element(by.id('email-input')).typeText('user@test.com')
|
|
114
|
+
await element(by.id('password-input')).typeText('wrongpass')
|
|
115
|
+
await element(by.id('login-button')).tap()
|
|
116
|
+
await expect(element(by.text('Invalid credentials'))).toBeVisible()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('navigates to home on success', async () => {
|
|
120
|
+
await element(by.id('email-input')).typeText('user@test.com')
|
|
121
|
+
await element(by.id('password-input')).typeText('correctpass')
|
|
122
|
+
await element(by.id('login-button')).tap()
|
|
123
|
+
await expect(element(by.id('home-screen'))).toBeVisible()
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**testID rules:**
|
|
129
|
+
```typescript
|
|
130
|
+
// Add testID to interactive elements
|
|
131
|
+
<TextInput testID="email-input" ... />
|
|
132
|
+
<TouchableOpacity testID="login-button" ... />
|
|
133
|
+
<View testID="home-screen" ... />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**What to test with Detox:**
|
|
137
|
+
- Login / logout flow
|
|
138
|
+
- Onboarding (first-time user)
|
|
139
|
+
- Critical purchase / checkout path
|
|
140
|
+
- Push notification tap → navigation
|
|
141
|
+
- Deep link handling
|
|
142
|
+
|
|
143
|
+
**What NOT to test with Detox:** minor UI variations, loading spinners, animations.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## E2E Testing — Maestro (Cross-Platform)
|
|
148
|
+
|
|
149
|
+
> Best for: Simpler setup, works on React Native + Flutter + native iOS/Android.
|
|
150
|
+
|
|
151
|
+
### Setup
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# macOS
|
|
155
|
+
brew tap mobile-dev-inc/tap
|
|
156
|
+
brew install maestro
|
|
157
|
+
|
|
158
|
+
# Run a flow
|
|
159
|
+
maestro test e2e/login.yaml
|
|
160
|
+
maestro test e2e/ # all flows in folder
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Write Maestro Flows (YAML)
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
# e2e/login.yaml
|
|
167
|
+
appId: com.myapp
|
|
168
|
+
---
|
|
169
|
+
- launchApp:
|
|
170
|
+
clearState: true
|
|
171
|
+
|
|
172
|
+
- assertVisible: "Sign In"
|
|
173
|
+
|
|
174
|
+
- tapOn:
|
|
175
|
+
id: "email-input"
|
|
176
|
+
- inputText: "user@test.com"
|
|
177
|
+
|
|
178
|
+
- tapOn:
|
|
179
|
+
id: "password-input"
|
|
180
|
+
- inputText: "wrongpass"
|
|
181
|
+
|
|
182
|
+
- tapOn:
|
|
183
|
+
id: "login-button"
|
|
184
|
+
|
|
185
|
+
- assertVisible: "Invalid credentials"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
# e2e/checkout.yaml
|
|
190
|
+
appId: com.myapp
|
|
191
|
+
---
|
|
192
|
+
- launchApp
|
|
193
|
+
- tapOn: "Products"
|
|
194
|
+
- tapOn:
|
|
195
|
+
index: 0 # first product
|
|
196
|
+
- tapOn: "Add to Cart"
|
|
197
|
+
- tapOn: "Checkout"
|
|
198
|
+
- assertVisible: "Order Confirmed"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Maestro Cloud CI
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Run on real devices in Maestro Cloud
|
|
205
|
+
maestro cloud --apiKey $MAESTRO_API_KEY e2e/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Maestro vs Detox:**
|
|
209
|
+
|
|
210
|
+
| | Maestro | Detox |
|
|
211
|
+
|--|---------|-------|
|
|
212
|
+
| Setup | Minutes | Hours |
|
|
213
|
+
| YAML / Code | YAML | TypeScript |
|
|
214
|
+
| Cross-platform | ✅ RN + Flutter + native | RN only |
|
|
215
|
+
| Speed | Slower | Faster |
|
|
216
|
+
| Power | Medium | High |
|
|
217
|
+
| CI integration | Maestro Cloud | Self-hosted |
|
|
218
|
+
|
|
219
|
+
**Use Maestro when:** simple flows, cross-platform team, quick setup.
|
|
220
|
+
**Use Detox when:** complex interactions, React Native only, full control.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Flutter Testing
|
|
225
|
+
|
|
226
|
+
### Unit + Widget Tests
|
|
227
|
+
|
|
228
|
+
```dart
|
|
229
|
+
// Unit test — business logic
|
|
230
|
+
test('CartBloc adds item correctly', () {
|
|
231
|
+
final bloc = CartBloc(cartRepository: MockCartRepository());
|
|
232
|
+
bloc.add(AddToCart(product: mockProduct));
|
|
233
|
+
expectLater(bloc.stream, emits(CartLoaded(items: [mockProduct])));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Widget test — UI
|
|
237
|
+
testWidgets('ProductCard shows title and price', (tester) async {
|
|
238
|
+
await tester.pumpWidget(MaterialApp(
|
|
239
|
+
home: ProductCard(product: mockProduct),
|
|
240
|
+
));
|
|
241
|
+
expect(find.text(mockProduct.title), findsOneWidget);
|
|
242
|
+
expect(find.text('\$${mockProduct.price}'), findsOneWidget);
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Integration Test (Flutter Driver replacement)
|
|
247
|
+
|
|
248
|
+
```dart
|
|
249
|
+
// integration_test/login_test.dart
|
|
250
|
+
void main() {
|
|
251
|
+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
252
|
+
|
|
253
|
+
testWidgets('login flow', (tester) async {
|
|
254
|
+
app.main();
|
|
255
|
+
await tester.pumpAndSettle();
|
|
256
|
+
|
|
257
|
+
await tester.enterText(find.byKey(Key('email')), 'user@test.com');
|
|
258
|
+
await tester.enterText(find.byKey(Key('password')), 'pass123');
|
|
259
|
+
await tester.tap(find.byKey(Key('login-button')));
|
|
260
|
+
await tester.pumpAndSettle();
|
|
261
|
+
|
|
262
|
+
expect(find.byKey(Key('home-screen')), findsOneWidget);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Run on simulator
|
|
269
|
+
flutter test integration_test/
|
|
270
|
+
|
|
271
|
+
# Run on real device
|
|
272
|
+
flutter test integration_test/ -d <device-id>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## iOS — XCUITest
|
|
278
|
+
|
|
279
|
+
```swift
|
|
280
|
+
func testLoginFlow() throws {
|
|
281
|
+
let app = XCUIApplication()
|
|
282
|
+
app.launch()
|
|
283
|
+
|
|
284
|
+
let emailField = app.textFields["email-input"]
|
|
285
|
+
emailField.tap()
|
|
286
|
+
emailField.typeText("user@test.com")
|
|
287
|
+
|
|
288
|
+
let passwordField = app.secureTextFields["password-input"]
|
|
289
|
+
passwordField.tap()
|
|
290
|
+
passwordField.typeText("pass123")
|
|
291
|
+
|
|
292
|
+
app.buttons["login-button"].tap()
|
|
293
|
+
|
|
294
|
+
XCTAssertTrue(app.otherElements["home-screen"].waitForExistence(timeout: 5))
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Android — Espresso
|
|
301
|
+
|
|
302
|
+
```kotlin
|
|
303
|
+
@Test
|
|
304
|
+
fun testLoginFlow() {
|
|
305
|
+
onView(withId(R.id.emailInput))
|
|
306
|
+
.perform(typeText("user@test.com"), closeSoftKeyboard())
|
|
307
|
+
onView(withId(R.id.passwordInput))
|
|
308
|
+
.perform(typeText("pass123"), closeSoftKeyboard())
|
|
309
|
+
onView(withId(R.id.loginButton)).perform(click())
|
|
310
|
+
onView(withId(R.id.homeScreen)).check(matches(isDisplayed()))
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Anti-Patterns
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
❌ Testing implementation details (internal state, private methods)
|
|
320
|
+
❌ Testing every UI pixel (visual regression belongs in Storybook/Percy)
|
|
321
|
+
❌ E2E for every edge case (unit test those)
|
|
322
|
+
❌ Skipping testID on interactive elements
|
|
323
|
+
❌ Running Detox without a stable test build
|
|
324
|
+
❌ Using sleep() instead of waitFor()
|
|
325
|
+
|
|
326
|
+
✅ Test user flows, not code internals
|
|
327
|
+
✅ E2E for critical paths: login, purchase, onboarding
|
|
328
|
+
✅ Unit test all business logic (hooks, slices, services)
|
|
329
|
+
✅ Mock API calls in unit tests
|
|
330
|
+
✅ Add testID to every tappable + input element
|
|
331
|
+
✅ waitFor() over sleep() in Detox
|
|
332
|
+
```
|
package/shared/on-device-ai.md
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# On-Device AI — Mobile ML Integration
|
|
2
|
-
|
|
3
|
-
> On-demand. Load when: "on-device AI", "ML model", "Core ML", "TFLite", "MediaPipe", "llama", "inference", "local model"
|
|
4
|
-
> Source: llama.cpp, Core ML, MediaPipe, TensorFlow Lite
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Decision Matrix
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
Use case Solution
|
|
12
|
-
───────────────────────────────────────────────────────────
|
|
13
|
-
Image classification / OCR Core ML (iOS) / ML Kit (Android)
|
|
14
|
-
Text classification / sentiment Core ML NLP / ML Kit
|
|
15
|
-
Face detection / pose estimation Vision (iOS) / MediaPipe
|
|
16
|
-
On-device LLM chat (<7B params) llama.cpp / llama.rn / executorch
|
|
17
|
-
Cloud LLM (>7B / latest models) API call — don't run on device
|
|
18
|
-
Real-time object detection Core ML / TFLite + MediaPipe
|
|
19
|
-
Speech to text (on-device) SFSpeechRecognizer (iOS) / ML Kit (Android)
|
|
20
|
-
|
|
21
|
-
Rule: If model > 500MB → use API. If latency > 3s acceptable → use API.
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## iOS — Core ML
|
|
27
|
-
|
|
28
|
-
```swift
|
|
29
|
-
// 1. Import model (drag .mlpackage into Xcode)
|
|
30
|
-
import CoreML
|
|
31
|
-
import Vision
|
|
32
|
-
|
|
33
|
-
// 2. Image classification
|
|
34
|
-
let model = try VNCoreMLModel(for: MyClassifier(configuration: .init()).model)
|
|
35
|
-
let request = VNCoreMLRequest(model: model) { request, _ in
|
|
36
|
-
guard let results = request.results as? [VNClassificationObservation] else { return }
|
|
37
|
-
let top = results.first!
|
|
38
|
-
print("\(top.identifier): \(top.confidence)")
|
|
39
|
-
}
|
|
40
|
-
let handler = VNImageRequestHandler(cgImage: image, options: [:])
|
|
41
|
-
try handler.perform([request])
|
|
42
|
-
|
|
43
|
-
// 3. NLP text classification
|
|
44
|
-
import NaturalLanguage
|
|
45
|
-
let classifier = NLModel(mlModel: SentimentClassifier().model)
|
|
46
|
-
let label = classifier.predictedLabel(for: "This is great!")
|
|
47
|
-
|
|
48
|
-
// Model conversion: use coremltools Python package
|
|
49
|
-
// coremltools.convert(pytorch_model, inputs=[...])
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## Android — ML Kit + MediaPipe
|
|
55
|
-
|
|
56
|
-
```kotlin
|
|
57
|
-
// ML Kit — text recognition (no model download needed)
|
|
58
|
-
dependencies {
|
|
59
|
-
implementation("com.google.mlkit:text-recognition:16.0.0")
|
|
60
|
-
implementation("com.google.mlkit:face-detection:16.1.5")
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
|
|
64
|
-
recognizer.process(inputImage)
|
|
65
|
-
.addOnSuccessListener { result -> result.text }
|
|
66
|
-
.addOnFailureListener { e -> /* handle */ }
|
|
67
|
-
|
|
68
|
-
// MediaPipe — pose / hand / face landmark detection
|
|
69
|
-
dependencies {
|
|
70
|
-
implementation("com.google.mediapipe:tasks-vision:0.10.14")
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
val handLandmarker = HandLandmarker.createFromOptions(context,
|
|
74
|
-
HandLandmarkerOptions.builder()
|
|
75
|
-
.setBaseOptions(BaseOptions.builder().setModelAssetPath("hand_landmarker.task").build())
|
|
76
|
-
.setNumHands(2)
|
|
77
|
-
.build()
|
|
78
|
-
)
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## On-Device LLM — llama.cpp (Cross-Platform)
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
Model sizes (GGUF Q4_K_M quantization):
|
|
87
|
-
Llama 3.2 3B → ~2GB RAM ✅ Phone-friendly
|
|
88
|
-
Llama 3.1 8B → ~5GB RAM ⚠️ High-end only (iPhone 15 Pro, Pixel 9)
|
|
89
|
-
Llama 3.1 70B → ~40GB RAM ❌ Not feasible on device
|
|
90
|
-
|
|
91
|
-
Download: huggingface.co/models?search=gguf
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
```swift
|
|
95
|
-
// iOS — llama.swift
|
|
96
|
-
// https://github.com/ggerganov/llama.cpp (Swift bindings included)
|
|
97
|
-
import llama
|
|
98
|
-
|
|
99
|
-
let model = llama_load_model_from_file(modelPath, llama_model_default_params())
|
|
100
|
-
let ctx = llama_new_context_with_model(model, llama_context_default_params())
|
|
101
|
-
// Tokenize + run inference on background thread
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
```javascript
|
|
105
|
-
// React Native — llama.rn
|
|
106
|
-
// npm install llama.rn
|
|
107
|
-
import { LlamaContext } from 'llama.rn';
|
|
108
|
-
|
|
109
|
-
const context = await LlamaContext.create({
|
|
110
|
-
model: `${RNFS.DocumentDirectoryPath}/model.gguf`,
|
|
111
|
-
n_ctx: 2048,
|
|
112
|
-
n_threads: 4,
|
|
113
|
-
});
|
|
114
|
-
const result = await context.completion({ prompt: 'Hello!', n_predict: 100 });
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
```dart
|
|
118
|
-
// Flutter — flutter_llama (or use Platform.channel to llama.cpp)
|
|
119
|
-
// For production: use executorch (Meta) or llama.cpp via FFI
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## React Native — ML Kit (via react-native-mlkit)
|
|
125
|
-
|
|
126
|
-
```javascript
|
|
127
|
-
// npm install @infinitered/react-native-mlkit-core
|
|
128
|
-
// npm install @infinitered/react-native-mlkit-object-detection
|
|
129
|
-
|
|
130
|
-
import { ObjectDetectionCamera } from '@infinitered/react-native-mlkit-object-detection';
|
|
131
|
-
|
|
132
|
-
// Image labeling
|
|
133
|
-
import MLKitImageLabeling from '@react-native-ml-kit/image-labeling';
|
|
134
|
-
const labels = await MLKitImageLabeling.label(imageUri);
|
|
135
|
-
// Returns: [{ text: 'Cat', confidence: 0.95 }]
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
## Flutter — tflite_flutter
|
|
141
|
-
|
|
142
|
-
```dart
|
|
143
|
-
// pubspec.yaml: tflite_flutter: ^0.10.4
|
|
144
|
-
import 'package:tflite_flutter/tflite_flutter.dart';
|
|
145
|
-
|
|
146
|
-
final interpreter = await Interpreter.fromAsset('model.tflite');
|
|
147
|
-
final input = [imageData]; // pre-processed tensor
|
|
148
|
-
final output = List.filled(1000, 0).reshape([1, 1000]);
|
|
149
|
-
interpreter.run(input, output);
|
|
150
|
-
// output[0] = probability for each class
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
## Performance Rules
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
1. NEVER run inference on the main thread
|
|
159
|
-
iOS: DispatchQueue.global(qos: .userInitiated).async { ... }
|
|
160
|
-
Android: viewModelScope.launch(Dispatchers.Default) { ... }
|
|
161
|
-
RN: run on JS thread or use NativeModule
|
|
162
|
-
|
|
163
|
-
2. Load model ONCE — cache in memory
|
|
164
|
-
❌ Load model on every inference call
|
|
165
|
-
✅ Load at app start or first use, keep reference
|
|
166
|
-
|
|
167
|
-
3. Batch requests when possible
|
|
168
|
-
- Process images in background queue, not per-tap
|
|
169
|
-
|
|
170
|
-
4. Show progress for operations >500ms
|
|
171
|
-
- Spinner or progress bar — user expects AI to take a moment
|
|
172
|
-
|
|
173
|
-
5. Fallback to API if device is low on memory
|
|
174
|
-
let memoryPressure = ProcessInfo.processInfo.isLowPowerModeEnabled
|
|
175
|
-
```
|