@buivietphi/skill-mobile-mt 1.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.
- package/AGENTS.md +392 -0
- package/README.md +224 -0
- package/SKILL.md +1048 -0
- package/android/android-native.md +208 -0
- package/bin/install.mjs +199 -0
- package/flutter/flutter.md +246 -0
- package/ios/ios-native.md +182 -0
- package/package.json +50 -0
- package/react-native/react-native.md +743 -0
- package/shared/agent-rules-template.md +343 -0
- package/shared/anti-patterns.md +407 -0
- package/shared/bug-detection.md +71 -0
- package/shared/claude-md-template.md +125 -0
- package/shared/code-review.md +121 -0
- package/shared/common-pitfalls.md +117 -0
- package/shared/document-analysis.md +167 -0
- package/shared/error-recovery.md +467 -0
- package/shared/observability.md +688 -0
- package/shared/performance-prediction.md +210 -0
- package/shared/platform-excellence.md +159 -0
- package/shared/prompt-engineering.md +677 -0
- package/shared/release-checklist.md +82 -0
- package/shared/version-management.md +509 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Android Native — Production Patterns
|
|
2
|
+
|
|
3
|
+
> Battle-tested patterns for Android Kotlin development.
|
|
4
|
+
> Multi-module Gradle, Hilt DI, Compose UI, offline-first.
|
|
5
|
+
> Also reference for RN/Flutter Android-side issues.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Clean Architecture (Multi-Module)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
project/
|
|
13
|
+
├── app/ # Main application module
|
|
14
|
+
│ ├── src/main/
|
|
15
|
+
│ │ ├── java/com/company/app/
|
|
16
|
+
│ │ │ ├── di/ # Hilt modules
|
|
17
|
+
│ │ │ ├── presentation/
|
|
18
|
+
│ │ │ │ ├── features/
|
|
19
|
+
│ │ │ │ │ ├── auth/
|
|
20
|
+
│ │ │ │ │ │ ├── ui/ # Composables
|
|
21
|
+
│ │ │ │ │ │ └── viewmodel/
|
|
22
|
+
│ │ │ │ │ └── home/
|
|
23
|
+
│ │ │ │ ├── navigation/
|
|
24
|
+
│ │ │ │ └── theme/
|
|
25
|
+
│ │ │ └── domain/
|
|
26
|
+
│ │ │ ├── model/ # Domain entities
|
|
27
|
+
│ │ │ ├── usecase/ # Business rules
|
|
28
|
+
│ │ │ └── repository/ # Repository interfaces
|
|
29
|
+
│ │ ├── res/
|
|
30
|
+
│ │ └── AndroidManifest.xml
|
|
31
|
+
│ └── build.gradle.kts
|
|
32
|
+
├── data/ # Data layer module
|
|
33
|
+
│ ├── src/main/java/
|
|
34
|
+
│ │ ├── repository/ # Repository implementations
|
|
35
|
+
│ │ ├── remote/ # API service, DTOs
|
|
36
|
+
│ │ ├── local/ # Room DAOs, entities
|
|
37
|
+
│ │ └── mapper/ # DTO ↔ Entity ↔ Domain mappers
|
|
38
|
+
│ └── build.gradle.kts
|
|
39
|
+
├── common/ # Shared utilities module
|
|
40
|
+
│ └── src/main/java/
|
|
41
|
+
├── build.gradle.kts # Root build file
|
|
42
|
+
├── settings.gradle.kts # Module declarations
|
|
43
|
+
└── gradle/
|
|
44
|
+
└── libs.versions.toml # Version catalog
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Dependency Rule
|
|
48
|
+
```
|
|
49
|
+
app (presentation) → domain/ ← data/
|
|
50
|
+
|
|
51
|
+
Presentation depends on Domain. Data depends on Domain.
|
|
52
|
+
Domain depends on NOTHING.
|
|
53
|
+
app module has access to all modules.
|
|
54
|
+
data module implements domain interfaces.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Compose UI Pattern
|
|
58
|
+
|
|
59
|
+
```kotlin
|
|
60
|
+
@Composable
|
|
61
|
+
fun ProductListScreen(
|
|
62
|
+
viewModel: ProductListViewModel = hiltViewModel(),
|
|
63
|
+
onProductClick: (String) -> Unit,
|
|
64
|
+
) {
|
|
65
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
66
|
+
|
|
67
|
+
Scaffold(
|
|
68
|
+
topBar = { TopAppBar(title = { Text("Products") }) },
|
|
69
|
+
) { padding ->
|
|
70
|
+
when (val state = uiState) {
|
|
71
|
+
is UiState.Loading -> Box(Modifier.fillMaxSize(), Alignment.Center) {
|
|
72
|
+
CircularProgressIndicator()
|
|
73
|
+
}
|
|
74
|
+
is UiState.Empty -> EmptyContent()
|
|
75
|
+
is UiState.Error -> ErrorContent(state.message, onRetry = viewModel::load)
|
|
76
|
+
is UiState.Success -> LazyColumn(
|
|
77
|
+
modifier = Modifier.padding(padding),
|
|
78
|
+
contentPadding = PaddingValues(16.dp),
|
|
79
|
+
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
80
|
+
) {
|
|
81
|
+
items(state.data, key = { it.id }) { product ->
|
|
82
|
+
ProductCard(product, onClick = { onProductClick(product.id) })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
sealed interface UiState<out T> {
|
|
90
|
+
data object Loading : UiState<Nothing>
|
|
91
|
+
data object Empty : UiState<Nothing>
|
|
92
|
+
data class Success<T>(val data: T) : UiState<T>
|
|
93
|
+
data class Error(val message: String) : UiState<Nothing>
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## ViewModel (Hilt)
|
|
98
|
+
|
|
99
|
+
```kotlin
|
|
100
|
+
@HiltViewModel
|
|
101
|
+
class ProductListViewModel @Inject constructor(
|
|
102
|
+
private val getProducts: GetProductsUseCase,
|
|
103
|
+
) : ViewModel() {
|
|
104
|
+
private val _uiState = MutableStateFlow<UiState<List<Product>>>(UiState.Loading)
|
|
105
|
+
val uiState = _uiState.asStateFlow()
|
|
106
|
+
|
|
107
|
+
init { load() }
|
|
108
|
+
|
|
109
|
+
fun load() {
|
|
110
|
+
viewModelScope.launch {
|
|
111
|
+
_uiState.value = UiState.Loading
|
|
112
|
+
getProducts()
|
|
113
|
+
.catch { _uiState.value = UiState.Error(it.message ?: "Error") }
|
|
114
|
+
.collect { items ->
|
|
115
|
+
_uiState.value = if (items.isEmpty()) UiState.Empty else UiState.Success(items)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## DI (Hilt)
|
|
123
|
+
|
|
124
|
+
```kotlin
|
|
125
|
+
@Module @InstallIn(SingletonComponent::class)
|
|
126
|
+
object NetworkModule {
|
|
127
|
+
@Provides @Singleton
|
|
128
|
+
fun provideRetrofit(): Retrofit = Retrofit.Builder()
|
|
129
|
+
.baseUrl(BuildConfig.API_BASE_URL)
|
|
130
|
+
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
|
|
131
|
+
.build()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Module @InstallIn(SingletonComponent::class)
|
|
135
|
+
abstract class RepositoryModule {
|
|
136
|
+
@Binds @Singleton
|
|
137
|
+
abstract fun bindProductRepo(impl: ProductRepositoryImpl): ProductRepository
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Data Layer (Retrofit + Room — Offline-First)
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
// data/remote/ProductApi.kt
|
|
145
|
+
interface ProductApi {
|
|
146
|
+
@GET("products") suspend fun getProducts(): ApiResponse<List<ProductDto>>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// data/local/ProductDao.kt
|
|
150
|
+
@Dao interface ProductDao {
|
|
151
|
+
@Query("SELECT * FROM products") fun getAll(): Flow<List<ProductEntity>>
|
|
152
|
+
@Upsert suspend fun upsertAll(items: List<ProductEntity>)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// data/repository/ProductRepositoryImpl.kt
|
|
156
|
+
class ProductRepositoryImpl @Inject constructor(
|
|
157
|
+
private val api: ProductApi, private val dao: ProductDao,
|
|
158
|
+
) : ProductRepository {
|
|
159
|
+
override fun getProducts(): Flow<List<Product>> = flow {
|
|
160
|
+
val cached = dao.getAll().first()
|
|
161
|
+
if (cached.isNotEmpty()) emit(cached.map { it.toDomain() })
|
|
162
|
+
try {
|
|
163
|
+
val fresh = api.getProducts()
|
|
164
|
+
dao.upsertAll(fresh.data.map { it.toEntity() })
|
|
165
|
+
} catch (e: Exception) { if (cached.isEmpty()) throw e }
|
|
166
|
+
emitAll(dao.getAll().map { it.map { e -> e.toDomain() } })
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Secure Storage
|
|
172
|
+
|
|
173
|
+
```kotlin
|
|
174
|
+
val prefs = EncryptedSharedPreferences.create(
|
|
175
|
+
context, "secure_prefs",
|
|
176
|
+
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
|
|
177
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
178
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Multi-Module Gradle Setup
|
|
183
|
+
|
|
184
|
+
```kotlin
|
|
185
|
+
// settings.gradle.kts
|
|
186
|
+
include(":app", ":data", ":common")
|
|
187
|
+
|
|
188
|
+
// app/build.gradle.kts
|
|
189
|
+
dependencies {
|
|
190
|
+
implementation(project(":data"))
|
|
191
|
+
implementation(project(":common"))
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Common Pitfalls
|
|
196
|
+
|
|
197
|
+
| Pitfall | Fix |
|
|
198
|
+
|---------|-----|
|
|
199
|
+
| `!!` assertion | `?.` / `?:` / `requireNotNull` |
|
|
200
|
+
| `collectAsState` | `collectAsStateWithLifecycle()` |
|
|
201
|
+
| Context leak | `@ApplicationContext`, never Activity |
|
|
202
|
+
| Missing ProGuard | Test release builds |
|
|
203
|
+
| Main thread blocking | `Dispatchers.IO` |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
> Multi-module Gradle + Hilt + Compose + offline-first.
|
|
208
|
+
> Clean Architecture with domain module having zero dependencies.
|
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @buivietphi/skill-mobile installer
|
|
5
|
+
*
|
|
6
|
+
* Installs skill-mobile-mt/ folder with subfolders for each platform.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx @buivietphi/skill-mobile # Interactive
|
|
10
|
+
* npx @buivietphi/skill-mobile --all # All detected agents
|
|
11
|
+
* npx @buivietphi/skill-mobile --claude # Claude Code only
|
|
12
|
+
* npx @buivietphi/skill-mobile --gemini # Gemini CLI
|
|
13
|
+
* npx @buivietphi/skill-mobile --kimi # Kimi
|
|
14
|
+
* npx @buivietphi/skill-mobile --antigravity # Antigravity
|
|
15
|
+
* npx @buivietphi/skill-mobile --auto # Auto-detect (postinstall)
|
|
16
|
+
* npx @buivietphi/skill-mobile --path DIR # Custom path
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, cpSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join, resolve, dirname } from 'node:path';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { createInterface } from 'node:readline';
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const PKG_ROOT = resolve(__dirname, '..');
|
|
27
|
+
const SKILL_NAME = 'skill-mobile-mt';
|
|
28
|
+
const HOME = homedir();
|
|
29
|
+
|
|
30
|
+
// Structure: root files + subfolders
|
|
31
|
+
const ROOT_FILES = ['SKILL.md', 'AGENTS.md'];
|
|
32
|
+
|
|
33
|
+
const SUBFOLDERS = {
|
|
34
|
+
'react-native': ['react-native.md'],
|
|
35
|
+
'flutter': ['flutter.md'],
|
|
36
|
+
'ios': ['ios-native.md'],
|
|
37
|
+
'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'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const AGENTS = {
|
|
42
|
+
claude: { name: 'Claude Code', dir: join(HOME, '.claude', 'skills'), detect: () => existsSync(join(HOME, '.claude')) },
|
|
43
|
+
codex: { name: 'Codex', dir: join(HOME, '.codex', 'skills'), detect: () => existsSync(join(HOME, '.codex')) },
|
|
44
|
+
gemini: { name: 'Gemini CLI', dir: join(HOME, '.gemini', 'skills'), detect: () => existsSync(join(HOME, '.gemini')) },
|
|
45
|
+
kimi: { name: 'Kimi', dir: join(HOME, '.kimi', 'skills'), detect: () => existsSync(join(HOME, '.kimi')) },
|
|
46
|
+
antigravity: { name: 'Antigravity', dir: join(HOME, '.agents', 'skills'), detect: () => existsSync(join(HOME, '.agents')) },
|
|
47
|
+
cursor: { name: 'Cursor', dir: join(HOME, '.cursor', 'skills'), detect: () => existsSync(join(HOME, '.cursor')) },
|
|
48
|
+
windsurf: { name: 'Windsurf', dir: join(HOME, '.windsurf', 'skills'), detect: () => existsSync(join(HOME, '.windsurf')) },
|
|
49
|
+
copilot: { name: 'Copilot', dir: join(HOME, '.copilot', 'skills'), detect: () => existsSync(join(HOME, '.copilot')) },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', red: '\x1b[31m' };
|
|
53
|
+
const log = m => console.log(m);
|
|
54
|
+
const ok = m => log(` ${c.green}✓${c.reset} ${m}`);
|
|
55
|
+
const info = m => log(` ${c.blue}ℹ${c.reset} ${m}`);
|
|
56
|
+
const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
|
|
57
|
+
|
|
58
|
+
function banner() {
|
|
59
|
+
log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────┐`);
|
|
60
|
+
log(` │ 📱 @buivietphi/skill-mobile-mt v1.0.0 │`);
|
|
61
|
+
log(` │ Master Senior Mobile Engineer │`);
|
|
62
|
+
log(` │ │`);
|
|
63
|
+
log(` │ Claude · Codex · Gemini · Kimi │`);
|
|
64
|
+
log(` │ Antigravity · Cursor · Windsurf · Copilot │`);
|
|
65
|
+
log(` │ React Native · Flutter · iOS · Android │`);
|
|
66
|
+
log(` └──────────────────────────────────────────────┘${c.reset}\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tokenCount(filePath) {
|
|
70
|
+
if (!existsSync(filePath)) return 0;
|
|
71
|
+
return Math.ceil(readFileSync(filePath, 'utf-8').length / 3.5);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function showContext() {
|
|
75
|
+
log(`${c.bold} 📊 Context:${c.reset}`);
|
|
76
|
+
let total = 0;
|
|
77
|
+
|
|
78
|
+
// Root files
|
|
79
|
+
for (const f of ROOT_FILES) {
|
|
80
|
+
const t = tokenCount(join(PKG_ROOT, f));
|
|
81
|
+
total += t;
|
|
82
|
+
log(` ${c.dim} ${f.padEnd(30)} ~${t.toLocaleString()} tokens${c.reset}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Subfolders
|
|
86
|
+
for (const [folder, files] of Object.entries(SUBFOLDERS)) {
|
|
87
|
+
let folderTotal = 0;
|
|
88
|
+
for (const f of files) {
|
|
89
|
+
folderTotal += tokenCount(join(PKG_ROOT, folder, f));
|
|
90
|
+
}
|
|
91
|
+
total += folderTotal;
|
|
92
|
+
log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${folderTotal.toLocaleString()} tokens${c.reset}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
log(`${c.dim} ─────────────────────────────────────────${c.reset}`);
|
|
96
|
+
log(` ${c.bold} All loaded:${c.reset} ~${total.toLocaleString()} tokens`);
|
|
97
|
+
log(` ${c.green} Smart load (1 platform):${c.reset} ~${Math.ceil(total * 0.55).toLocaleString()} tokens\n`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function install(baseDir, agentName) {
|
|
101
|
+
const dst = join(baseDir, SKILL_NAME);
|
|
102
|
+
mkdirSync(dst, { recursive: true });
|
|
103
|
+
let n = 0;
|
|
104
|
+
|
|
105
|
+
// Copy root files
|
|
106
|
+
for (const f of ROOT_FILES) {
|
|
107
|
+
const src = join(PKG_ROOT, f);
|
|
108
|
+
if (!existsSync(src)) continue;
|
|
109
|
+
cpSync(src, join(dst, f), { force: true });
|
|
110
|
+
n++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Copy subfolders
|
|
114
|
+
for (const [folder, files] of Object.entries(SUBFOLDERS)) {
|
|
115
|
+
const dstFolder = join(dst, folder);
|
|
116
|
+
mkdirSync(dstFolder, { recursive: true });
|
|
117
|
+
for (const f of files) {
|
|
118
|
+
const src = join(PKG_ROOT, folder, f);
|
|
119
|
+
if (!existsSync(src)) continue;
|
|
120
|
+
cpSync(src, join(dstFolder, f), { force: true });
|
|
121
|
+
n++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ok(`${c.bold}${SKILL_NAME}/${c.reset} → ${agentName} ${c.dim}(${dst})${c.reset}`);
|
|
126
|
+
return n;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function ask(q) {
|
|
130
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
131
|
+
return new Promise(r => { rl.question(q, a => { rl.close(); r(a.trim()); }); });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
const flags = new Set(args.map(a => a.replace(/^--?/, '')));
|
|
137
|
+
|
|
138
|
+
banner();
|
|
139
|
+
showContext();
|
|
140
|
+
|
|
141
|
+
let targets = [];
|
|
142
|
+
|
|
143
|
+
if (flags.has('all')) targets = Object.keys(AGENTS);
|
|
144
|
+
else if (flags.has('auto')) {
|
|
145
|
+
targets = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
|
|
146
|
+
if (!targets.length) { info('No agents found. Using Claude.'); targets = ['claude']; }
|
|
147
|
+
} else if (flags.has('path')) {
|
|
148
|
+
const p = args[args.indexOf('--path') + 1];
|
|
149
|
+
if (!p) { fail('--path needs dir'); process.exit(1); }
|
|
150
|
+
install(resolve(p), 'Custom');
|
|
151
|
+
log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`); return;
|
|
152
|
+
} else {
|
|
153
|
+
for (const k of Object.keys(AGENTS)) if (flags.has(k)) targets.push(k);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!targets.length) {
|
|
157
|
+
const det = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
|
|
158
|
+
log(`${c.bold} Detected agents:${c.reset}`);
|
|
159
|
+
det.forEach(k => log(` ${c.green}●${c.reset} ${AGENTS[k].name}`));
|
|
160
|
+
Object.keys(AGENTS).filter(k => !det.includes(k)).forEach(k => log(` ${c.dim}○ ${AGENTS[k].name}${c.reset}`));
|
|
161
|
+
log('');
|
|
162
|
+
const a = await ask(' Install to detected agents? [Y/n] ');
|
|
163
|
+
if (a.toLowerCase() === 'n') { info('Cancelled.'); return; }
|
|
164
|
+
targets = det.length ? det : ['claude'];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
log(`\n${c.bold} Installing...${c.reset}\n`);
|
|
168
|
+
for (const k of targets) install(AGENTS[k].dir, AGENTS[k].name);
|
|
169
|
+
|
|
170
|
+
log(`\n${c.green}${c.bold} ✅ Done!${c.reset} → ${targets.length} agent(s)\n`);
|
|
171
|
+
log(` ${c.bold}Usage:${c.reset}`);
|
|
172
|
+
log(` ${c.cyan}@skill-mobile-mt${c.reset} Pre-built patterns (18 production apps)`);
|
|
173
|
+
log(` ${c.cyan}@skill-mobile-mt project${c.reset} Read current project, adapt to it\n`);
|
|
174
|
+
log(` ${c.bold}Installed structure:${c.reset}`);
|
|
175
|
+
log(` ${SKILL_NAME}/`);
|
|
176
|
+
log(` ├── SKILL.md Entry point + auto-detect`);
|
|
177
|
+
log(` ├── AGENTS.md Multi-agent config`);
|
|
178
|
+
log(` ├── react-native/`);
|
|
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`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main().catch(e => { fail(e.message); process.exit(1); });
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Flutter — Production Patterns
|
|
2
|
+
|
|
3
|
+
> Battle-tested patterns from production Flutter apps.
|
|
4
|
+
> State: Riverpod (standard)
|
|
5
|
+
> DI: get_it + injectable
|
|
6
|
+
> Networking: Dio + Retrofit, or http
|
|
7
|
+
> Navigation: GoRouter
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Clean Architecture
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
lib/
|
|
15
|
+
├── main.dart # Entry: Firebase init, Hive init, ProviderScope
|
|
16
|
+
├── app/
|
|
17
|
+
│ ├── app.dart # MaterialApp.router
|
|
18
|
+
│ ├── router.dart # GoRouter config
|
|
19
|
+
│ └── theme/
|
|
20
|
+
├── features/
|
|
21
|
+
│ ├── auth/
|
|
22
|
+
│ │ ├── domain/ # Entities + use cases + repository interfaces
|
|
23
|
+
│ │ ├── data/ # Repository impl, API client, DTOs, mappers
|
|
24
|
+
│ │ ├── presentation/ # Screens + widgets
|
|
25
|
+
│ │ └── providers/ # Riverpod providers
|
|
26
|
+
│ └── [feature]/
|
|
27
|
+
│ └── ... (same structure)
|
|
28
|
+
├── shared/
|
|
29
|
+
│ ├── widgets/ # Reusable UI
|
|
30
|
+
│ ├── extensions/
|
|
31
|
+
│ └── utils/
|
|
32
|
+
└── core/
|
|
33
|
+
├── network/ # Dio client setup, interceptors
|
|
34
|
+
├── storage/ # Hive + SharedPrefs + flutter_secure_storage
|
|
35
|
+
├── di/ # get_it + injectable setup
|
|
36
|
+
└── constants/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Dependency Rule
|
|
40
|
+
```
|
|
41
|
+
presentation/ → domain/ ← data/
|
|
42
|
+
|
|
43
|
+
Presentation depends on Domain. Data depends on Domain.
|
|
44
|
+
Domain depends on NOTHING.
|
|
45
|
+
Never import data/ from presentation/ directly.
|
|
46
|
+
Use cases call repository interfaces (defined in domain/).
|
|
47
|
+
Data layer provides implementations.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## State Management (Riverpod)
|
|
51
|
+
|
|
52
|
+
```dart
|
|
53
|
+
// domain/usecases/get_products.dart
|
|
54
|
+
class GetProducts {
|
|
55
|
+
final ProductRepository repository;
|
|
56
|
+
GetProducts(this.repository);
|
|
57
|
+
Future<List<Product>> call() => repository.getProducts();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// features/product/providers/product_providers.dart
|
|
61
|
+
@riverpod
|
|
62
|
+
class ProductList extends _$ProductList {
|
|
63
|
+
@override
|
|
64
|
+
FutureOr<List<Product>> build() async {
|
|
65
|
+
final repo = ref.read(productRepositoryProvider);
|
|
66
|
+
return repo.getProducts();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Future<void> refresh() async {
|
|
70
|
+
state = const AsyncLoading();
|
|
71
|
+
state = await AsyncValue.guard(() async {
|
|
72
|
+
final repo = ref.read(productRepositoryProvider);
|
|
73
|
+
return repo.getProducts();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// features/product/presentation/product_screen.dart
|
|
79
|
+
class ProductScreen extends ConsumerWidget {
|
|
80
|
+
const ProductScreen({super.key});
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
84
|
+
final products = ref.watch(productListProvider);
|
|
85
|
+
return products.when(
|
|
86
|
+
data: (list) => list.isEmpty
|
|
87
|
+
? const EmptyState()
|
|
88
|
+
: ListView.builder(
|
|
89
|
+
itemCount: list.length,
|
|
90
|
+
itemBuilder: (_, i) => ProductCard(product: list[i]),
|
|
91
|
+
),
|
|
92
|
+
loading: () => const Center(child: CircularProgressIndicator()),
|
|
93
|
+
error: (e, _) => ErrorState(onRetry: () => ref.invalidate(productListProvider)),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Dependency Injection (get_it + injectable)
|
|
100
|
+
|
|
101
|
+
```dart
|
|
102
|
+
// core/di/injection.dart
|
|
103
|
+
import 'package:get_it/get_it.dart';
|
|
104
|
+
import 'package:injectable/injectable.dart';
|
|
105
|
+
|
|
106
|
+
final getIt = GetIt.instance;
|
|
107
|
+
|
|
108
|
+
@InjectableInit()
|
|
109
|
+
void configureDependencies() => getIt.init();
|
|
110
|
+
|
|
111
|
+
// Usage: annotate classes
|
|
112
|
+
@injectable
|
|
113
|
+
class AuthRepository {
|
|
114
|
+
final ApiClient _client;
|
|
115
|
+
AuthRepository(this._client);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Navigation (GoRouter)
|
|
120
|
+
|
|
121
|
+
```dart
|
|
122
|
+
final router = GoRouter(
|
|
123
|
+
redirect: (context, state) {
|
|
124
|
+
final isAuth = authNotifier.isAuthenticated;
|
|
125
|
+
final isAuthRoute = state.matchedLocation.startsWith('/auth');
|
|
126
|
+
if (!isAuth && !isAuthRoute) return '/auth/login';
|
|
127
|
+
if (isAuth && isAuthRoute) return '/';
|
|
128
|
+
return null;
|
|
129
|
+
},
|
|
130
|
+
routes: [
|
|
131
|
+
ShellRoute(
|
|
132
|
+
builder: (_, __, child) => MainScaffold(child: child),
|
|
133
|
+
routes: [
|
|
134
|
+
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
|
|
135
|
+
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
|
|
136
|
+
],
|
|
137
|
+
),
|
|
138
|
+
GoRoute(path: '/auth/login', builder: (_, __) => const LoginScreen()),
|
|
139
|
+
],
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Networking
|
|
144
|
+
|
|
145
|
+
### Dio + Retrofit
|
|
146
|
+
|
|
147
|
+
```dart
|
|
148
|
+
@RestApi(baseUrl: ApiConstants.baseUrl)
|
|
149
|
+
abstract class ApiClient {
|
|
150
|
+
factory ApiClient(Dio dio) = _ApiClient;
|
|
151
|
+
|
|
152
|
+
@GET('/products')
|
|
153
|
+
Future<ApiResponse<List<ProductDto>>> getProducts();
|
|
154
|
+
|
|
155
|
+
@POST('/auth/login')
|
|
156
|
+
Future<ApiResponse<AuthDto>> login(@Body() LoginInput input);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Dio setup with interceptors
|
|
160
|
+
final dio = Dio(BaseOptions(
|
|
161
|
+
baseUrl: ApiConstants.baseUrl,
|
|
162
|
+
connectTimeout: const Duration(seconds: 15),
|
|
163
|
+
))
|
|
164
|
+
..interceptors.addAll([
|
|
165
|
+
AuthInterceptor(secureStorage),
|
|
166
|
+
PrettyDioLogger(requestBody: true, responseBody: true),
|
|
167
|
+
RetryInterceptor(dio: dio, retries: 2),
|
|
168
|
+
]);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### HTTP (simpler pattern)
|
|
172
|
+
|
|
173
|
+
```dart
|
|
174
|
+
final response = await http.get(
|
|
175
|
+
Uri.parse('$baseUrl/endpoint'),
|
|
176
|
+
headers: {'Authorization': 'Bearer $token'},
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Local Storage
|
|
181
|
+
|
|
182
|
+
```dart
|
|
183
|
+
// Hive (structured data)
|
|
184
|
+
await Hive.initFlutter();
|
|
185
|
+
Hive.registerAdapter(UserAdapter());
|
|
186
|
+
final box = await Hive.openBox<User>('users');
|
|
187
|
+
|
|
188
|
+
// Floor (Room-like SQL)
|
|
189
|
+
@dao
|
|
190
|
+
abstract class ProductDao {
|
|
191
|
+
@Query('SELECT * FROM products')
|
|
192
|
+
Future<List<ProductEntity>> getAll();
|
|
193
|
+
|
|
194
|
+
@Insert(onConflict: OnConflictStrategy.replace)
|
|
195
|
+
Future<void> insertAll(List<ProductEntity> products);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// SharedPreferences (simple key-value)
|
|
199
|
+
final prefs = await SharedPreferences.getInstance();
|
|
200
|
+
|
|
201
|
+
// flutter_secure_storage (tokens)
|
|
202
|
+
final storage = const FlutterSecureStorage();
|
|
203
|
+
await storage.write(key: 'token', value: token);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Firebase
|
|
207
|
+
|
|
208
|
+
```dart
|
|
209
|
+
// Firebase init in main.dart
|
|
210
|
+
await Firebase.initializeApp();
|
|
211
|
+
await FirebaseMessaging.instance.requestPermission();
|
|
212
|
+
|
|
213
|
+
// Firestore
|
|
214
|
+
final doc = await FirebaseFirestore.instance.collection('users').doc(uid).get();
|
|
215
|
+
|
|
216
|
+
// FCM push
|
|
217
|
+
FirebaseMessaging.onMessage.listen((message) {
|
|
218
|
+
// Handle foreground notification
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Key Libraries
|
|
223
|
+
|
|
224
|
+
| Purpose | Library |
|
|
225
|
+
|---------|---------|
|
|
226
|
+
| State | flutter_riverpod |
|
|
227
|
+
| DI | get_it + injectable |
|
|
228
|
+
| HTTP | dio + retrofit |
|
|
229
|
+
| HTTP | http |
|
|
230
|
+
| DB | floor |
|
|
231
|
+
| DB | hive |
|
|
232
|
+
| Router | go_router |
|
|
233
|
+
| UI | flutter_screenutil |
|
|
234
|
+
| Anim | flutter_animate |
|
|
235
|
+
| Auth | local_auth (biometric) |
|
|
236
|
+
| Crypto | encrypt, crypto |
|
|
237
|
+
| Forms | formz |
|
|
238
|
+
| Func | dartz (Either, Option) |
|
|
239
|
+
| Models | freezed_annotation |
|
|
240
|
+
| Firebase | firebase_core, messaging, firestore |
|
|
241
|
+
| Image | cached_network_image |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
> Standard: Riverpod + get_it/injectable + Clean Architecture.
|
|
246
|
+
> Dio/Retrofit for complex APIs. Floor for offline-first. Firebase for push/analytics.
|