@buivietphi/skill-mobile-mt 1.0.0 → 1.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 +1 -0
- package/SKILL.md +5 -3
- package/bin/install.mjs +121 -71
- package/package.json +1 -1
- package/shared/architecture-intelligence.md +416 -0
package/AGENTS.md
CHANGED
|
@@ -53,6 +53,7 @@ skill-mobile-mt/
|
|
|
53
53
|
├── platform-excellence.md ← iOS 18+ vs Android 15+ UX (1,500 tokens)
|
|
54
54
|
├── version-management.md ← SDK compat matrix + release testing (3,500 tokens)
|
|
55
55
|
├── observability.md ← Sessions as 4th pillar (3,000 tokens)
|
|
56
|
+
├── architecture-intelligence.md ← Patterns from 30+ production repos (4,500 tokens)
|
|
56
57
|
├── common-pitfalls.md ← Known issue patterns (1,160 tokens)
|
|
57
58
|
├── release-checklist.md ← App Store/Play Store checklist (587 tokens)
|
|
58
59
|
│
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-mobile-mt
|
|
3
|
-
description: "Master Senior Mobile Engineer. 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.
|
|
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.1.0"
|
|
5
5
|
author: buivietphi
|
|
6
6
|
priority: high
|
|
7
7
|
user-invocable: true
|
|
@@ -72,7 +72,9 @@ USER REQUEST → ACTION (Read tool required)
|
|
|
72
72
|
|
|
73
73
|
"Add X to existing Y" → MODIFY existing files, don't create new structure
|
|
74
74
|
|
|
75
|
-
"Setup project / architecture" → Read
|
|
75
|
+
"Setup project / architecture" → Read: shared/architecture-intelligence.md
|
|
76
|
+
then: Read platform file (see Smart Loading below)
|
|
77
|
+
then: suggest structure based on project size + stack
|
|
76
78
|
|
|
77
79
|
"Fix / debug X" → Read: shared/bug-detection.md
|
|
78
80
|
then: read code → find root cause → fix → verify
|
package/bin/install.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Installs skill-mobile-mt/ folder with subfolders for each platform.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* npx @buivietphi/skill-mobile # Interactive
|
|
9
|
+
* npx @buivietphi/skill-mobile # Interactive checkbox UI
|
|
10
10
|
* npx @buivietphi/skill-mobile --all # All detected agents
|
|
11
11
|
* npx @buivietphi/skill-mobile --claude # Claude Code only
|
|
12
12
|
* npx @buivietphi/skill-mobile --gemini # Gemini CLI
|
|
@@ -20,7 +20,6 @@ import { existsSync, mkdirSync, cpSync, readFileSync } from 'node:fs';
|
|
|
20
20
|
import { join, resolve, dirname } from 'node:path';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import { createInterface } from 'node:readline';
|
|
24
23
|
|
|
25
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
25
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
@@ -35,29 +34,29 @@ const SUBFOLDERS = {
|
|
|
35
34
|
'flutter': ['flutter.md'],
|
|
36
35
|
'ios': ['ios-native.md'],
|
|
37
36
|
'android': ['android-native.md'],
|
|
38
|
-
'shared': ['code-review.md', 'bug-detection.md', 'prompt-engineering.md', 'release-checklist.md', 'common-pitfalls.md', 'error-recovery.md', 'document-analysis.md', 'anti-patterns.md', 'performance-prediction.md', 'platform-excellence.md', 'version-management.md', 'observability.md', 'claude-md-template.md', 'agent-rules-template.md'],
|
|
37
|
+
'shared': ['code-review.md', 'bug-detection.md', 'prompt-engineering.md', 'release-checklist.md', 'common-pitfalls.md', 'error-recovery.md', 'document-analysis.md', 'anti-patterns.md', 'performance-prediction.md', 'platform-excellence.md', 'version-management.md', 'observability.md', 'architecture-intelligence.md', 'claude-md-template.md', 'agent-rules-template.md'],
|
|
39
38
|
};
|
|
40
39
|
|
|
41
40
|
const AGENTS = {
|
|
42
|
-
claude: { name: 'Claude Code',
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
claude: { name: 'Claude Code', dir: join(HOME, '.claude', 'skills'), detect: () => existsSync(join(HOME, '.claude')) },
|
|
42
|
+
cursor: { name: 'Cursor', dir: join(HOME, '.cursor', 'skills'), detect: () => existsSync(join(HOME, '.cursor')) },
|
|
43
|
+
windsurf: { name: 'Windsurf', dir: join(HOME, '.windsurf', 'skills'), detect: () => existsSync(join(HOME, '.windsurf')) },
|
|
44
|
+
copilot: { name: 'Copilot', dir: join(HOME, '.copilot', 'skills'), detect: () => existsSync(join(HOME, '.copilot')) },
|
|
45
|
+
codex: { name: 'Codex', dir: join(HOME, '.codex', 'skills'), detect: () => existsSync(join(HOME, '.codex')) },
|
|
46
|
+
gemini: { name: 'Gemini CLI', dir: join(HOME, '.gemini', 'skills'), detect: () => existsSync(join(HOME, '.gemini')) },
|
|
47
|
+
kimi: { name: 'Kimi', dir: join(HOME, '.kimi', 'skills'), detect: () => existsSync(join(HOME, '.kimi')) },
|
|
48
|
+
antigravity: { name: 'Antigravity', dir: join(HOME, '.agents', 'skills'), detect: () => existsSync(join(HOME, '.agents')) },
|
|
50
49
|
};
|
|
51
50
|
|
|
52
|
-
const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
|
|
53
|
-
const log = m =>
|
|
54
|
-
const ok
|
|
51
|
+
const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', red: '\x1b[31m' };
|
|
52
|
+
const log = m => process.stdout.write(m + '\n');
|
|
53
|
+
const ok = m => log(` ${c.green}✓${c.reset} ${m}`);
|
|
55
54
|
const info = m => log(` ${c.blue}ℹ${c.reset} ${m}`);
|
|
56
55
|
const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
|
|
57
56
|
|
|
58
57
|
function banner() {
|
|
59
58
|
log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────┐`);
|
|
60
|
-
log(` │ 📱 @buivietphi/skill-mobile-mt v1.
|
|
59
|
+
log(` │ 📱 @buivietphi/skill-mobile-mt v1.1.0 │`);
|
|
61
60
|
log(` │ Master Senior Mobile Engineer │`);
|
|
62
61
|
log(` │ │`);
|
|
63
62
|
log(` │ Claude · Codex · Gemini · Kimi │`);
|
|
@@ -72,26 +71,19 @@ function tokenCount(filePath) {
|
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
function showContext() {
|
|
75
|
-
log(`${c.bold} 📊 Context:${c.reset}`);
|
|
74
|
+
log(`${c.bold} 📊 Context budget:${c.reset}`);
|
|
76
75
|
let total = 0;
|
|
77
|
-
|
|
78
|
-
// Root files
|
|
79
76
|
for (const f of ROOT_FILES) {
|
|
80
77
|
const t = tokenCount(join(PKG_ROOT, f));
|
|
81
78
|
total += t;
|
|
82
79
|
log(` ${c.dim} ${f.padEnd(30)} ~${t.toLocaleString()} tokens${c.reset}`);
|
|
83
80
|
}
|
|
84
|
-
|
|
85
|
-
// Subfolders
|
|
86
81
|
for (const [folder, files] of Object.entries(SUBFOLDERS)) {
|
|
87
|
-
let
|
|
88
|
-
for (const f of files)
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
total += folderTotal;
|
|
92
|
-
log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${folderTotal.toLocaleString()} tokens${c.reset}`);
|
|
82
|
+
let ft = 0;
|
|
83
|
+
for (const f of files) ft += tokenCount(join(PKG_ROOT, folder, f));
|
|
84
|
+
total += ft;
|
|
85
|
+
log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${ft.toLocaleString()} tokens${c.reset}`);
|
|
93
86
|
}
|
|
94
|
-
|
|
95
87
|
log(`${c.dim} ─────────────────────────────────────────${c.reset}`);
|
|
96
88
|
log(` ${c.bold} All loaded:${c.reset} ~${total.toLocaleString()} tokens`);
|
|
97
89
|
log(` ${c.green} Smart load (1 platform):${c.reset} ~${Math.ceil(total * 0.55).toLocaleString()} tokens\n`);
|
|
@@ -101,16 +93,12 @@ function install(baseDir, agentName) {
|
|
|
101
93
|
const dst = join(baseDir, SKILL_NAME);
|
|
102
94
|
mkdirSync(dst, { recursive: true });
|
|
103
95
|
let n = 0;
|
|
104
|
-
|
|
105
|
-
// Copy root files
|
|
106
96
|
for (const f of ROOT_FILES) {
|
|
107
97
|
const src = join(PKG_ROOT, f);
|
|
108
98
|
if (!existsSync(src)) continue;
|
|
109
99
|
cpSync(src, join(dst, f), { force: true });
|
|
110
100
|
n++;
|
|
111
101
|
}
|
|
112
|
-
|
|
113
|
-
// Copy subfolders
|
|
114
102
|
for (const [folder, files] of Object.entries(SUBFOLDERS)) {
|
|
115
103
|
const dstFolder = join(dst, folder);
|
|
116
104
|
mkdirSync(dstFolder, { recursive: true });
|
|
@@ -121,18 +109,95 @@ function install(baseDir, agentName) {
|
|
|
121
109
|
n++;
|
|
122
110
|
}
|
|
123
111
|
}
|
|
124
|
-
|
|
125
112
|
ok(`${c.bold}${SKILL_NAME}/${c.reset} → ${agentName} ${c.dim}(${dst})${c.reset}`);
|
|
126
113
|
return n;
|
|
127
114
|
}
|
|
128
115
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
// ─── Checkbox UI ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
async function selectAgents(detected) {
|
|
119
|
+
if (!process.stdin.isTTY) {
|
|
120
|
+
return detected.length ? detected : ['claude'];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const keys = Object.keys(AGENTS);
|
|
124
|
+
const selected = new Set(detected); // pre-tick detected agents
|
|
125
|
+
let cursor = 0;
|
|
126
|
+
|
|
127
|
+
const UP = '\x1b[A';
|
|
128
|
+
const DOWN = '\x1b[B';
|
|
129
|
+
const ERASE_DN = '\x1b[J';
|
|
130
|
+
const HIDE_CUR = '\x1b[?25l';
|
|
131
|
+
const SHOW_CUR = '\x1b[?25h';
|
|
132
|
+
const moveUp = n => `\x1b[${n}A`;
|
|
133
|
+
|
|
134
|
+
// header = 3 lines (title + hint + blank), items = keys.length
|
|
135
|
+
const TOTAL_LINES = 3 + keys.length;
|
|
136
|
+
|
|
137
|
+
function render(first) {
|
|
138
|
+
if (!first) process.stdout.write(moveUp(TOTAL_LINES) + ERASE_DN);
|
|
139
|
+
|
|
140
|
+
process.stdout.write(`\n${c.bold} Select agents to install:${c.reset}\n`);
|
|
141
|
+
process.stdout.write(` ${c.dim}↑↓ navigate Space toggle A select all Enter confirm Q cancel${c.reset}\n`);
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < keys.length; i++) {
|
|
144
|
+
const k = keys[i];
|
|
145
|
+
const agent = AGENTS[k];
|
|
146
|
+
const isCur = i === cursor;
|
|
147
|
+
const isSel = selected.has(k);
|
|
148
|
+
const isDet = detected.includes(k);
|
|
149
|
+
|
|
150
|
+
const ptr = isCur ? `${c.cyan}›${c.reset}` : ' ';
|
|
151
|
+
const box = isSel ? `${c.green}◉${c.reset}` : `${c.dim}◯${c.reset}`;
|
|
152
|
+
const name = isCur ? `${c.bold}${c.cyan}${agent.name}${c.reset}` : agent.name;
|
|
153
|
+
const badge = isDet ? ` ${c.green}[detected]${c.reset}` : `${c.dim} [not found]${c.reset}`;
|
|
154
|
+
|
|
155
|
+
process.stdout.write(` ${ptr} ${box} ${name.padEnd(14)}${badge}\n`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
process.stdout.write(HIDE_CUR);
|
|
160
|
+
render(true);
|
|
161
|
+
|
|
162
|
+
return new Promise(resolve => {
|
|
163
|
+
process.stdin.setRawMode(true);
|
|
164
|
+
process.stdin.resume();
|
|
165
|
+
process.stdin.setEncoding('utf8');
|
|
166
|
+
|
|
167
|
+
const onKey = key => {
|
|
168
|
+
const done = result => {
|
|
169
|
+
process.stdin.setRawMode(false);
|
|
170
|
+
process.stdin.pause();
|
|
171
|
+
process.stdin.off('data', onKey);
|
|
172
|
+
process.stdout.write(SHOW_CUR + '\n');
|
|
173
|
+
resolve(result);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (key === '\x03') { done(null); process.exit(0); } // Ctrl+C
|
|
177
|
+
if (key === 'q' || key === 'Q' || key === '\x1b') { done([]); return; } // quit
|
|
178
|
+
if (key === '\r' || key === '\n') { done([...selected]); return; } // Enter
|
|
179
|
+
|
|
180
|
+
if (key === UP) cursor = (cursor - 1 + keys.length) % keys.length;
|
|
181
|
+
else if (key === DOWN) cursor = (cursor + 1) % keys.length;
|
|
182
|
+
else if (key === ' ') {
|
|
183
|
+
if (selected.has(keys[cursor])) selected.delete(keys[cursor]);
|
|
184
|
+
else selected.add(keys[cursor]);
|
|
185
|
+
} else if (key === 'a' || key === 'A') {
|
|
186
|
+
if (selected.size === keys.length) selected.clear();
|
|
187
|
+
else keys.forEach(k => selected.add(k));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
render(false);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
process.stdin.on('data', onKey);
|
|
194
|
+
});
|
|
132
195
|
}
|
|
133
196
|
|
|
197
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
134
199
|
async function main() {
|
|
135
|
-
const args
|
|
200
|
+
const args = process.argv.slice(2);
|
|
136
201
|
const flags = new Set(args.map(a => a.replace(/^--?/, '')));
|
|
137
202
|
|
|
138
203
|
banner();
|
|
@@ -140,28 +205,31 @@ async function main() {
|
|
|
140
205
|
|
|
141
206
|
let targets = [];
|
|
142
207
|
|
|
143
|
-
if (flags.has('all'))
|
|
144
|
-
|
|
208
|
+
if (flags.has('all')) {
|
|
209
|
+
targets = Object.keys(AGENTS);
|
|
210
|
+
} else if (flags.has('auto')) {
|
|
145
211
|
targets = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
|
|
146
212
|
if (!targets.length) { info('No agents found. Using Claude.'); targets = ['claude']; }
|
|
147
213
|
} else if (flags.has('path')) {
|
|
148
214
|
const p = args[args.indexOf('--path') + 1];
|
|
149
|
-
if (!p) { fail('--path needs
|
|
215
|
+
if (!p) { fail('--path needs a directory'); process.exit(1); }
|
|
150
216
|
install(resolve(p), 'Custom');
|
|
151
|
-
log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`);
|
|
217
|
+
log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`);
|
|
218
|
+
return;
|
|
152
219
|
} else {
|
|
153
220
|
for (const k of Object.keys(AGENTS)) if (flags.has(k)) targets.push(k);
|
|
154
221
|
}
|
|
155
222
|
|
|
223
|
+
// Interactive checkbox when no flag given
|
|
156
224
|
if (!targets.length) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
targets =
|
|
225
|
+
const detected = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
|
|
226
|
+
const chosen = await selectAgents(detected);
|
|
227
|
+
|
|
228
|
+
if (!chosen || chosen.length === 0) {
|
|
229
|
+
info('Cancelled.');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
targets = chosen;
|
|
165
233
|
}
|
|
166
234
|
|
|
167
235
|
log(`\n${c.bold} Installing...${c.reset}\n`);
|
|
@@ -171,29 +239,11 @@ async function main() {
|
|
|
171
239
|
log(` ${c.bold}Usage:${c.reset}`);
|
|
172
240
|
log(` ${c.cyan}@skill-mobile-mt${c.reset} Pre-built patterns (18 production apps)`);
|
|
173
241
|
log(` ${c.cyan}@skill-mobile-mt project${c.reset} Read current project, adapt to it\n`);
|
|
174
|
-
log(` ${c.bold}Installed
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
log(
|
|
179
|
-
log(` │ └── react-native.md React Native patterns`);
|
|
180
|
-
log(` ├── flutter/`);
|
|
181
|
-
log(` │ └── flutter.md Flutter patterns`);
|
|
182
|
-
log(` ├── ios/`);
|
|
183
|
-
log(` │ └── ios-native.md iOS Swift patterns`);
|
|
184
|
-
log(` ├── android/`);
|
|
185
|
-
log(` │ └── android-native.md Android Kotlin patterns`);
|
|
186
|
-
log(` └── shared/`);
|
|
187
|
-
log(` ├── code-review.md Review checklist`);
|
|
188
|
-
log(` ├── bug-detection.md Bug scanner`);
|
|
189
|
-
log(` ├── prompt-engineering.md Auto-think`);
|
|
190
|
-
log(` ├── anti-patterns.md PII/cardinality detection`);
|
|
191
|
-
log(` ├── performance-prediction.md Frame budget calculator`);
|
|
192
|
-
log(` ├── platform-excellence.md iOS 18+ vs Android 15+`);
|
|
193
|
-
log(` ├── version-management.md SDK compatibility matrix`);
|
|
194
|
-
log(` ├── observability.md Sessions as 4th pillar`);
|
|
195
|
-
log(` ├── claude-md-template.md CLAUDE.md template for Claude Code`);
|
|
196
|
-
log(` └── agent-rules-template.md Rules template for ALL agents\n`);
|
|
242
|
+
log(` ${c.bold}Installed to:${c.reset}`);
|
|
243
|
+
for (const k of targets) {
|
|
244
|
+
log(` ${c.green}●${c.reset} ${AGENTS[k].name.padEnd(14)} ${c.dim}${AGENTS[k].dir}/${SKILL_NAME}${c.reset}`);
|
|
245
|
+
}
|
|
246
|
+
log('');
|
|
197
247
|
}
|
|
198
248
|
|
|
199
249
|
main().catch(e => { fail(e.message); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buivietphi/skill-mobile-mt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# Architecture Intelligence — Patterns from 30+ Production Repos
|
|
2
|
+
|
|
3
|
+
> On-demand. Load when: "architecture", "structure", "setup project", "best practices", "how to organize"
|
|
4
|
+
> Source: Analyzed 30+ open-source production mobile apps (total 200k+ GitHub stars)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Reference Repos (by platform)
|
|
9
|
+
|
|
10
|
+
| Platform | Repo | Stars | Key Pattern |
|
|
11
|
+
|----------|------|-------|-------------|
|
|
12
|
+
| **RN** | Ignite (infinitered) | 19.7k | MST + MMKV + generators |
|
|
13
|
+
| **RN** | Obytes Template | 4k | Zustand + TanStack Query + Expo Router |
|
|
14
|
+
| **RN** | Expensify/App | 4.7k | Onyx custom state + centralized constants |
|
|
15
|
+
| **RN** | Mattermost Mobile | 2.6k | WatermelonDB + offline-first + WebSocket |
|
|
16
|
+
| **RN** | Artsy Eigen | 3.8k | Relay/GraphQL + Scene pattern |
|
|
17
|
+
| **Flutter** | Immich | 93.5k | Riverpod + drift + auto_route + clean arch |
|
|
18
|
+
| **Flutter** | AppFlowy | 68.2k | BLoC + GetIt + startup/ pattern |
|
|
19
|
+
| **Flutter** | Spotube | 44.6k | Riverpod + Hooks + custom hooks folder |
|
|
20
|
+
| **Flutter** | Hiddify | 26.7k | Riverpod + go_router + bootstrap.dart |
|
|
21
|
+
| **Flutter** | Ente Photos | 24.8k | Melos monorepo + gateway pattern |
|
|
22
|
+
| **iOS** | TCA (Point-Free) | 14.4k | Unidirectional + TestStore + @Dependency |
|
|
23
|
+
| **iOS** | Clean Arch SwiftUI | 6.5k | Redux state + @Environment DI |
|
|
24
|
+
| **iOS** | Modern Clean Arch | 4.1k | Tuist + 5-layer DDD + MVVM+TCA coexist |
|
|
25
|
+
| **Android** | Now in Android | 20.7k | Official Google arch + no-mock testing |
|
|
26
|
+
| **Android** | Android Showcase | 6.7k | Konsist validation + Koin DI |
|
|
27
|
+
| **Android** | Mihon | 18.8k | Plugin/extension arch + MVI |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Cross-Platform Architecture Patterns
|
|
32
|
+
|
|
33
|
+
### 1. Dual State Management (Client + Server)
|
|
34
|
+
|
|
35
|
+
**The pattern:** Separate client state (UI, forms) from server state (API cache).
|
|
36
|
+
|
|
37
|
+
| Platform | Client State | Server State |
|
|
38
|
+
|----------|-------------|--------------|
|
|
39
|
+
| React Native | Zustand / MST | TanStack Query |
|
|
40
|
+
| Flutter | Riverpod | Riverpod + drift |
|
|
41
|
+
| iOS | @Observable / TCA | URLSession cache |
|
|
42
|
+
| Android | ViewModel + Flow | Repository + Room |
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// RN: Zustand for client + TanStack Query for server
|
|
46
|
+
const useAuthStore = create((set) => ({
|
|
47
|
+
token: null,
|
|
48
|
+
setToken: (t) => set({ token: t }),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const { data, isLoading } = useQuery({
|
|
52
|
+
queryKey: ['products'],
|
|
53
|
+
queryFn: () => api.getProducts(),
|
|
54
|
+
staleTime: 5 * 60 * 1000,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```dart
|
|
59
|
+
// Flutter: Riverpod for both, but separated
|
|
60
|
+
@riverpod
|
|
61
|
+
class AuthNotifier extends _$AuthNotifier { ... } // client
|
|
62
|
+
|
|
63
|
+
@riverpod
|
|
64
|
+
Future<List<Product>> products(Ref ref) async { ... } // server
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Feature-Based Module Organization
|
|
68
|
+
|
|
69
|
+
**Every top repo (100%) uses feature-based organization, NOT type-based.**
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
// ✅ CORRECT: Feature-based (Immich, Obytes, Hiddify, Now in Android)
|
|
73
|
+
src/features/
|
|
74
|
+
auth/
|
|
75
|
+
domain/ # entities, use cases, repo interfaces
|
|
76
|
+
data/ # repo impl, API, DTOs, mappers
|
|
77
|
+
presentation/ # screens, widgets, viewmodels
|
|
78
|
+
|
|
79
|
+
// ❌ WRONG: Type-based (no production app uses this)
|
|
80
|
+
src/
|
|
81
|
+
models/
|
|
82
|
+
services/
|
|
83
|
+
screens/
|
|
84
|
+
widgets/
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Centralized Constants Pattern (Expensify)
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// src/ROUTES.ts — Prevents string duplication
|
|
91
|
+
const ROUTES = {
|
|
92
|
+
HOME: 'home',
|
|
93
|
+
SETTINGS: 'settings',
|
|
94
|
+
PROFILE: 'profile/:id',
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
// src/SCREENS.ts — Screen component names
|
|
98
|
+
const SCREENS = {
|
|
99
|
+
HOME: 'HomeScreen',
|
|
100
|
+
SETTINGS: 'SettingsScreen',
|
|
101
|
+
} as const;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. Bootstrap / Startup Pattern (Hiddify, AppFlowy)
|
|
105
|
+
|
|
106
|
+
```dart
|
|
107
|
+
// bootstrap.dart — Clean app initialization
|
|
108
|
+
Future<void> bootstrap() async {
|
|
109
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
110
|
+
|
|
111
|
+
// 1. Core services
|
|
112
|
+
await Firebase.initializeApp();
|
|
113
|
+
await Hive.initFlutter();
|
|
114
|
+
|
|
115
|
+
// 2. DI registration
|
|
116
|
+
setupServiceLocator();
|
|
117
|
+
|
|
118
|
+
// 3. Run app
|
|
119
|
+
runApp(
|
|
120
|
+
ProviderScope(
|
|
121
|
+
observers: [RiverpodObserver()],
|
|
122
|
+
child: const App(),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// main.dart
|
|
128
|
+
void main() => bootstrap();
|
|
129
|
+
|
|
130
|
+
// main_prod.dart — Production flavor
|
|
131
|
+
void main() {
|
|
132
|
+
const env = Environment.production;
|
|
133
|
+
bootstrap(env: env);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 5. Draft Pairs for Forms (Expensify)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// Every form has FORM + FORM_DRAFT for unsaved changes
|
|
141
|
+
const ONYX_KEYS = {
|
|
142
|
+
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
|
|
143
|
+
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
|
|
144
|
+
} as const;
|
|
145
|
+
|
|
146
|
+
// Save draft on every keystroke → restore on crash/back
|
|
147
|
+
// Only commit FORM when user explicitly saves
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 6. Database Subscription Pattern (Mattermost)
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
database/
|
|
154
|
+
models/ # WatermelonDB model definitions
|
|
155
|
+
schema/ # DB structure + versioning
|
|
156
|
+
migration/ # Schema migrations
|
|
157
|
+
operator/ # Complex query logic (keeps models clean)
|
|
158
|
+
subscription/ # Reactive UI updates from DB changes
|
|
159
|
+
exceptions/ # Custom error classes
|
|
160
|
+
manager/ # DB lifecycle (create, reset, destroy)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 7. Functional Error Handling (Flutter — dartz)
|
|
164
|
+
|
|
165
|
+
```dart
|
|
166
|
+
// Instead of try/catch everywhere, use Either<Failure, Success>
|
|
167
|
+
Future<Either<AppException, User>> getUser(String id) async {
|
|
168
|
+
try {
|
|
169
|
+
final response = await dio.get('/users/$id');
|
|
170
|
+
return Right(User.fromJson(response.data));
|
|
171
|
+
} on DioException catch (e) {
|
|
172
|
+
return Left(NetworkException(e.message));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Usage — forces caller to handle both cases
|
|
177
|
+
final result = await getUser('123');
|
|
178
|
+
result.fold(
|
|
179
|
+
(failure) => showError(failure.message),
|
|
180
|
+
(user) => showProfile(user),
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 8. Architecture Validation (Android Showcase — Konsist)
|
|
185
|
+
|
|
186
|
+
```kotlin
|
|
187
|
+
// Programmatically enforce architecture rules
|
|
188
|
+
@Test
|
|
189
|
+
fun `domain layer should not depend on data layer`() {
|
|
190
|
+
Konsist.scopeFromModule("feature-album/domain")
|
|
191
|
+
.classes()
|
|
192
|
+
.assertFalse { it.hasImport { import -> import.hasNameContaining("data") } }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Test
|
|
196
|
+
fun `use cases should have 'UseCase' suffix`() {
|
|
197
|
+
Konsist.scopeFromModule("feature-album/domain")
|
|
198
|
+
.classes()
|
|
199
|
+
.withNameContaining("UseCase")
|
|
200
|
+
.assertTrue { it.hasPublicFunction("invoke") }
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Platform-Specific Intelligence
|
|
207
|
+
|
|
208
|
+
### React Native — Must-Know Patterns
|
|
209
|
+
|
|
210
|
+
| Pattern | Old Way | New Way (2024-2025) | Source |
|
|
211
|
+
|---------|---------|---------------------|--------|
|
|
212
|
+
| Storage | AsyncStorage | MMKV (60x faster) | Ignite, Obytes |
|
|
213
|
+
| Routing | React Navigation | Expo Router (file-based) | Obytes |
|
|
214
|
+
| Server state | Redux + thunk | TanStack Query | Obytes, TCM |
|
|
215
|
+
| Client state | Redux | Zustand | Obytes |
|
|
216
|
+
| E2E testing | Detox | Maestro | Ignite, Obytes |
|
|
217
|
+
| Forms | Formik + Yup | TanStack Form + Zod | Obytes |
|
|
218
|
+
| Styling | StyleSheet | NativeWind (TailwindCSS) | Obytes |
|
|
219
|
+
| Animations | Animated API | Reanimated 3 | All |
|
|
220
|
+
| Images | Image | expo-image / FastImage | All |
|
|
221
|
+
|
|
222
|
+
### Flutter — Must-Know Patterns
|
|
223
|
+
|
|
224
|
+
| Pattern | Old Way | New Way (2024-2025) | Source |
|
|
225
|
+
|---------|---------|---------------------|--------|
|
|
226
|
+
| State | setState / Provider | Riverpod + code-gen | Immich, Hiddify, Spotube |
|
|
227
|
+
| State (alt) | BLoC manual | BLoC + freezed | AppFlowy |
|
|
228
|
+
| Navigation | Navigator 2.0 | auto_route / go_router | Immich / Hiddify |
|
|
229
|
+
| Database | sqflite | drift (type-safe ORM) | Immich, Spotube |
|
|
230
|
+
| Models | manual fromJson | freezed + json_serializable | All |
|
|
231
|
+
| i18n | .arb files | slang (type-safe, generated) | Hiddify |
|
|
232
|
+
| HTTP | http package | dio + smart_retry | Hiddify |
|
|
233
|
+
| HTTP (perf) | dio on all | cronet (Android) + cupertino_http (iOS) | Immich |
|
|
234
|
+
| Monorepo | N/A | Melos | Ente |
|
|
235
|
+
| DI | manual | Riverpod providers / GetIt | Immich / AppFlowy |
|
|
236
|
+
|
|
237
|
+
### iOS Swift — Must-Know Patterns
|
|
238
|
+
|
|
239
|
+
| Pattern | Traditional | Modern (2024-2025) | Source |
|
|
240
|
+
|---------|-------------|---------------------|--------|
|
|
241
|
+
| Architecture | MVVM manual | TCA (macro-driven) | Point-Free |
|
|
242
|
+
| DI | Swinject | @Dependency (Point-Free) / @Environment | TCA / nalexn |
|
|
243
|
+
| Testing | XCTest + mocks | TestStore (deterministic) | TCA |
|
|
244
|
+
| Navigation | NavigationView | NavigationStack + Coordinator | sergdort |
|
|
245
|
+
| Modularization | One target | Tuist multi-module | sergdort |
|
|
246
|
+
| Data binding | Combine | @Observable macro | iOS 17+ |
|
|
247
|
+
| SwiftUI testing | None | ViewInspector | nalexn |
|
|
248
|
+
| Concurrency | GCD / Combine | async/await + actors | All |
|
|
249
|
+
|
|
250
|
+
### Android Kotlin — Must-Know Patterns
|
|
251
|
+
|
|
252
|
+
| Pattern | Traditional | Modern (2024-2025) | Source |
|
|
253
|
+
|---------|-------------|---------------------|--------|
|
|
254
|
+
| UI | XML Views | Jetpack Compose | All |
|
|
255
|
+
| Architecture | MVVM | MVVM + UDF (Now in Android) | Google |
|
|
256
|
+
| DI | Dagger 2 | Hilt | Now in Android |
|
|
257
|
+
| DI (lightweight) | N/A | Koin | Android Showcase |
|
|
258
|
+
| Testing | Mockito | No-mock test doubles | Now in Android |
|
|
259
|
+
| Arch validation | Manual review | Konsist | Android Showcase |
|
|
260
|
+
| Performance | ProGuard | Baseline Profiles + R8 | Now in Android |
|
|
261
|
+
| Navigation | Fragment nav | Compose Navigation (type-safe) | All |
|
|
262
|
+
| Build config | build.gradle | Convention Plugins | Now in Android |
|
|
263
|
+
| Screenshot test | None | Roborazzi | Now in Android |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Production Folder Structure Templates
|
|
268
|
+
|
|
269
|
+
### React Native (Expo Router — based on Obytes)
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
src/
|
|
273
|
+
app/ # Expo Router file-based routes
|
|
274
|
+
(app)/ # Authenticated group
|
|
275
|
+
_layout.tsx
|
|
276
|
+
(tabs)/ # Tab navigator
|
|
277
|
+
login.tsx
|
|
278
|
+
onboarding.tsx
|
|
279
|
+
_layout.tsx # Root layout
|
|
280
|
+
features/ # Feature modules
|
|
281
|
+
auth/
|
|
282
|
+
hooks/
|
|
283
|
+
components/
|
|
284
|
+
services/
|
|
285
|
+
types.ts
|
|
286
|
+
products/
|
|
287
|
+
hooks/
|
|
288
|
+
components/
|
|
289
|
+
services/
|
|
290
|
+
components/
|
|
291
|
+
ui/ # Design system (Button, Input, Card)
|
|
292
|
+
lib/
|
|
293
|
+
api/ # Axios + TanStack Query setup
|
|
294
|
+
auth/ # Token management (Zustand + MMKV)
|
|
295
|
+
hooks/ # Shared hooks
|
|
296
|
+
i18n/ # Internationalization
|
|
297
|
+
storage.ts # MMKV wrapper
|
|
298
|
+
utils.ts # Utilities
|
|
299
|
+
translations/ # Language files
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Flutter (Riverpod + Clean — based on Immich)
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
lib/
|
|
306
|
+
main.dart
|
|
307
|
+
bootstrap.dart # App initialization
|
|
308
|
+
app/
|
|
309
|
+
app.dart # MaterialApp.router
|
|
310
|
+
router.dart # auto_route / go_router config
|
|
311
|
+
theme/
|
|
312
|
+
features/
|
|
313
|
+
auth/
|
|
314
|
+
domain/ # Entities, use cases, repo interfaces
|
|
315
|
+
data/ # Repo impl, datasources, DTOs
|
|
316
|
+
presentation/ # Screens + widgets
|
|
317
|
+
providers/ # Riverpod providers
|
|
318
|
+
[feature]/
|
|
319
|
+
shared/
|
|
320
|
+
widgets/ # Reusable UI
|
|
321
|
+
extensions/ # Dart extensions
|
|
322
|
+
constants/ # App-wide constants
|
|
323
|
+
interfaces/ # Abstract contracts
|
|
324
|
+
models/ # Shared DTOs
|
|
325
|
+
utils/ # Utilities
|
|
326
|
+
l10n/ # Localization
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### iOS SwiftUI (TCA — based on Point-Free + sergdort)
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
App/
|
|
333
|
+
AppDelegate.swift
|
|
334
|
+
AppModule.swift # Composition root
|
|
335
|
+
Features/
|
|
336
|
+
Auth/
|
|
337
|
+
AuthFeature.swift # @Reducer
|
|
338
|
+
AuthView.swift # SwiftUI View
|
|
339
|
+
AuthClient.swift # @Dependency
|
|
340
|
+
Home/
|
|
341
|
+
HomeFeature.swift
|
|
342
|
+
HomeView.swift
|
|
343
|
+
Settings/
|
|
344
|
+
Shared/
|
|
345
|
+
Models/
|
|
346
|
+
Extensions/
|
|
347
|
+
UI/ # Shared SwiftUI components
|
|
348
|
+
Clients/ # API, Storage, Keychain
|
|
349
|
+
Platform/
|
|
350
|
+
Networking/
|
|
351
|
+
Persistence/
|
|
352
|
+
Tests/
|
|
353
|
+
AuthFeatureTests.swift # TestStore tests
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Android Kotlin (Now in Android — based on Google reference)
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
app/ # Main application
|
|
360
|
+
core/
|
|
361
|
+
common/ # Shared utilities
|
|
362
|
+
data/ # Repository implementations
|
|
363
|
+
database/ # Room database
|
|
364
|
+
datastore/ # Proto DataStore
|
|
365
|
+
network/ # Retrofit + OkHttp
|
|
366
|
+
model/ # Core models
|
|
367
|
+
ui/ # Shared Compose components
|
|
368
|
+
testing/ # Test utilities
|
|
369
|
+
feature/
|
|
370
|
+
auth/
|
|
371
|
+
src/main/
|
|
372
|
+
AuthScreen.kt # Compose UI
|
|
373
|
+
AuthViewModel.kt # ViewModel
|
|
374
|
+
AuthUiState.kt # UI state sealed class
|
|
375
|
+
navigation/ # Feature nav graph
|
|
376
|
+
home/
|
|
377
|
+
settings/
|
|
378
|
+
build-logic/ # Convention plugins
|
|
379
|
+
convention/
|
|
380
|
+
AndroidApplicationConventionPlugin.kt
|
|
381
|
+
AndroidFeatureConventionPlugin.kt
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Decision Matrix — When to Use What
|
|
387
|
+
|
|
388
|
+
### State Management
|
|
389
|
+
|
|
390
|
+
| Condition | RN | Flutter | iOS | Android |
|
|
391
|
+
|-----------|-----|---------|-----|---------|
|
|
392
|
+
| Simple app (<10 screens) | Zustand | Riverpod | @Observable | ViewModel + StateFlow |
|
|
393
|
+
| Complex app (10-50 screens) | Zustand + TanStack Query | Riverpod + freezed | TCA | MVVM + Hilt + Flow |
|
|
394
|
+
| Enterprise (50+ screens) | Onyx (custom) / Redux Toolkit | BLoC + GetIt | TCA + Tuist | MVVM + Hilt + Convention Plugins |
|
|
395
|
+
| Offline-first | TanStack Query + WatermelonDB | Riverpod + drift | TCA + SwiftData | Room + WorkManager |
|
|
396
|
+
|
|
397
|
+
### Navigation
|
|
398
|
+
|
|
399
|
+
| Condition | RN | Flutter | iOS | Android |
|
|
400
|
+
|-----------|-----|---------|-----|---------|
|
|
401
|
+
| Expo project | Expo Router | — | — | — |
|
|
402
|
+
| RN CLI project | React Navigation | — | — | — |
|
|
403
|
+
| Type-safe routes | — | auto_route | NavigationStack | Compose Navigation |
|
|
404
|
+
| Declarative | Expo Router | go_router | NavigationStack | Compose Navigation |
|
|
405
|
+
| Deep linking | Expo Linking | app_links | Universal Links | App Links |
|
|
406
|
+
|
|
407
|
+
### Testing
|
|
408
|
+
|
|
409
|
+
| What | RN | Flutter | iOS | Android |
|
|
410
|
+
|------|-----|---------|-----|---------|
|
|
411
|
+
| Unit | Jest | test + mocktail | XCTest / TestStore | JUnit + no-mock doubles |
|
|
412
|
+
| Widget/Component | React Testing Library | widget test | ViewInspector | Compose Testing |
|
|
413
|
+
| E2E | Maestro | integration_test | XCUITest | Macrobenchmark |
|
|
414
|
+
| Screenshot | — | golden_toolkit | — | Roborazzi |
|
|
415
|
+
| Architecture | Dependency Cruiser | — | — | Konsist |
|
|
416
|
+
| Performance | Reassure | DevTools | Instruments | Baseline Profiles |
|