@zigrivers/scaffold 3.7.0 → 3.9.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.
Files changed (97) hide show
  1. package/README.md +113 -8
  2. package/content/knowledge/browser-extension/browser-extension-architecture.md +195 -0
  3. package/content/knowledge/browser-extension/browser-extension-content-scripts.md +264 -0
  4. package/content/knowledge/browser-extension/browser-extension-conventions.md +156 -0
  5. package/content/knowledge/browser-extension/browser-extension-cross-browser.md +229 -0
  6. package/content/knowledge/browser-extension/browser-extension-dev-environment.md +247 -0
  7. package/content/knowledge/browser-extension/browser-extension-manifest.md +220 -0
  8. package/content/knowledge/browser-extension/browser-extension-project-structure.md +183 -0
  9. package/content/knowledge/browser-extension/browser-extension-requirements.md +107 -0
  10. package/content/knowledge/browser-extension/browser-extension-security.md +202 -0
  11. package/content/knowledge/browser-extension/browser-extension-service-workers.md +265 -0
  12. package/content/knowledge/browser-extension/browser-extension-store-submission.md +155 -0
  13. package/content/knowledge/browser-extension/browser-extension-testing.md +270 -0
  14. package/content/knowledge/data-pipeline/data-pipeline-architecture.md +175 -0
  15. package/content/knowledge/data-pipeline/data-pipeline-batch-patterns.md +263 -0
  16. package/content/knowledge/data-pipeline/data-pipeline-conventions.md +176 -0
  17. package/content/knowledge/data-pipeline/data-pipeline-dev-environment.md +350 -0
  18. package/content/knowledge/data-pipeline/data-pipeline-orchestration.md +291 -0
  19. package/content/knowledge/data-pipeline/data-pipeline-project-structure.md +257 -0
  20. package/content/knowledge/data-pipeline/data-pipeline-quality.md +324 -0
  21. package/content/knowledge/data-pipeline/data-pipeline-requirements.md +145 -0
  22. package/content/knowledge/data-pipeline/data-pipeline-schema-management.md +295 -0
  23. package/content/knowledge/data-pipeline/data-pipeline-security.md +326 -0
  24. package/content/knowledge/data-pipeline/data-pipeline-streaming-patterns.md +280 -0
  25. package/content/knowledge/data-pipeline/data-pipeline-testing.md +406 -0
  26. package/content/knowledge/library/library-api-design.md +306 -0
  27. package/content/knowledge/library/library-architecture.md +247 -0
  28. package/content/knowledge/library/library-bundling.md +244 -0
  29. package/content/knowledge/library/library-conventions.md +229 -0
  30. package/content/knowledge/library/library-dev-environment.md +220 -0
  31. package/content/knowledge/library/library-documentation.md +300 -0
  32. package/content/knowledge/library/library-project-structure.md +237 -0
  33. package/content/knowledge/library/library-requirements.md +173 -0
  34. package/content/knowledge/library/library-security.md +257 -0
  35. package/content/knowledge/library/library-testing.md +319 -0
  36. package/content/knowledge/library/library-type-definitions.md +284 -0
  37. package/content/knowledge/library/library-versioning.md +300 -0
  38. package/content/knowledge/ml/ml-architecture.md +172 -0
  39. package/content/knowledge/ml/ml-conventions.md +209 -0
  40. package/content/knowledge/ml/ml-dev-environment.md +299 -0
  41. package/content/knowledge/ml/ml-experiment-tracking.md +285 -0
  42. package/content/knowledge/ml/ml-model-evaluation.md +256 -0
  43. package/content/knowledge/ml/ml-observability.md +253 -0
  44. package/content/knowledge/ml/ml-project-structure.md +216 -0
  45. package/content/knowledge/ml/ml-requirements.md +138 -0
  46. package/content/knowledge/ml/ml-security.md +188 -0
  47. package/content/knowledge/ml/ml-serving-patterns.md +243 -0
  48. package/content/knowledge/ml/ml-testing.md +301 -0
  49. package/content/knowledge/ml/ml-training-patterns.md +269 -0
  50. package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
  51. package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
  52. package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
  53. package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
  54. package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
  55. package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
  56. package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
  57. package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
  58. package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
  59. package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
  60. package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
  61. package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
  62. package/content/methodology/browser-extension-overlay.yml +82 -0
  63. package/content/methodology/data-pipeline-overlay.yml +70 -0
  64. package/content/methodology/library-overlay.yml +67 -0
  65. package/content/methodology/ml-overlay.yml +70 -0
  66. package/content/methodology/mobile-app-overlay.yml +71 -0
  67. package/dist/cli/commands/init.d.ts +22 -0
  68. package/dist/cli/commands/init.d.ts.map +1 -1
  69. package/dist/cli/commands/init.js +202 -3
  70. package/dist/cli/commands/init.js.map +1 -1
  71. package/dist/cli/commands/init.test.js +190 -0
  72. package/dist/cli/commands/init.test.js.map +1 -1
  73. package/dist/config/schema.d.ts +1456 -80
  74. package/dist/config/schema.d.ts.map +1 -1
  75. package/dist/config/schema.js +87 -0
  76. package/dist/config/schema.js.map +1 -1
  77. package/dist/config/schema.test.js +312 -3
  78. package/dist/config/schema.test.js.map +1 -1
  79. package/dist/core/assembly/overlay-loader.test.js +55 -0
  80. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  81. package/dist/e2e/project-type-overlays.test.d.ts +2 -1
  82. package/dist/e2e/project-type-overlays.test.d.ts.map +1 -1
  83. package/dist/e2e/project-type-overlays.test.js +780 -14
  84. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  85. package/dist/types/config.d.ts +16 -1
  86. package/dist/types/config.d.ts.map +1 -1
  87. package/dist/wizard/questions.d.ts +28 -1
  88. package/dist/wizard/questions.d.ts.map +1 -1
  89. package/dist/wizard/questions.js +127 -1
  90. package/dist/wizard/questions.js.map +1 -1
  91. package/dist/wizard/questions.test.js +224 -4
  92. package/dist/wizard/questions.test.js.map +1 -1
  93. package/dist/wizard/wizard.d.ts +22 -0
  94. package/dist/wizard/wizard.d.ts.map +1 -1
  95. package/dist/wizard/wizard.js +28 -1
  96. package/dist/wizard/wizard.js.map +1 -1
  97. package/package.json +1 -1
@@ -0,0 +1,269 @@
1
+ ---
2
+ name: ml-training-patterns
3
+ description: Data loaders, training loops, distributed training with DDP and FSDP, checkpointing strategies, and hyperparameter tuning patterns
4
+ topics: [ml, training, data-loaders, distributed-training, ddp, fsdp, checkpointing, hyperparameter-tuning]
5
+ ---
6
+
7
+ The training loop is the heart of every ML project, but it is also where most bugs hide: data leaking between splits, gradients not zeroed, mixed precision overflows, checkpoints saved incorrectly, and distributed training hanging on a single slow worker. These are not exotic edge cases — they are the standard bugs that every ML engineer encounters. A well-structured training pipeline prevents them through clear separation of concerns and defensive coding.
8
+
9
+ ## Summary
10
+
11
+ Build training pipelines with properly configured data loaders (worker count, pinned memory, prefetch), clean training loops with explicit gradient management, mixed precision for efficiency, and robust checkpointing. For large models or large datasets, use PyTorch DDP for multi-GPU training or FSDP for models too large to fit on a single GPU. Manage hyperparameter search with a systematic tool (Optuna, Ray Tune, W&B Sweeps) rather than manual iteration.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Data Loaders
16
+
17
+ `torch.utils.data.DataLoader` is the standard interface for batched data loading. Configure it correctly:
18
+
19
+ ```python
20
+ from torch.utils.data import DataLoader
21
+ from src.data.dataset import MyDataset
22
+
23
+ def build_dataloader(
24
+ dataset: MyDataset,
25
+ batch_size: int,
26
+ split: str,
27
+ num_workers: int = 4,
28
+ ) -> DataLoader:
29
+ is_train = split == "train"
30
+ return DataLoader(
31
+ dataset,
32
+ batch_size=batch_size,
33
+ shuffle=is_train, # Shuffle only training data
34
+ num_workers=num_workers, # Parallel data loading workers
35
+ pin_memory=True, # Pin CPU memory for faster GPU transfer
36
+ prefetch_factor=2, # Prefetch 2 batches per worker
37
+ persistent_workers=True, # Keep workers alive between epochs
38
+ drop_last=is_train, # Drop incomplete final batch (training only)
39
+ )
40
+ ```
41
+
42
+ **`num_workers` guidance**:
43
+ - Start with `min(os.cpu_count(), 8)` and tune from there
44
+ - Set to 0 for debugging (single-process, easier stack traces)
45
+ - On Windows, set to 0 if you encounter multiprocessing issues
46
+ - Bottleneck check: if GPU utilisation < 80%, increase workers or enable prefetch
47
+
48
+ **Common data loader bugs**:
49
+ - Using `shuffle=True` on validation/test sets (breaks reproducibility checks)
50
+ - Not setting `worker_init_fn` when using random augmentation in workers (workers share the same seed without this)
51
+ - `pin_memory=True` on a machine without GPU (no-op but wastes memory)
52
+
53
+ ### Training Loop Structure
54
+
55
+ ```python
56
+ def train_epoch(
57
+ model: nn.Module,
58
+ loader: DataLoader,
59
+ optimizer: torch.optim.Optimizer,
60
+ criterion: nn.Module,
61
+ scaler: torch.cuda.amp.GradScaler,
62
+ device: torch.device,
63
+ ) -> dict[str, float]:
64
+ model.train()
65
+ total_loss = 0.0
66
+ n_batches = 0
67
+
68
+ for batch in loader:
69
+ inputs, targets = batch
70
+ inputs = inputs.to(device, non_blocking=True)
71
+ targets = targets.to(device, non_blocking=True)
72
+
73
+ # Zero gradients BEFORE forward pass
74
+ optimizer.zero_grad(set_to_none=True) # Faster than zero_grad()
75
+
76
+ # Mixed precision forward pass
77
+ with torch.autocast(device_type="cuda", dtype=torch.float16):
78
+ outputs = model(inputs)
79
+ loss = criterion(outputs, targets)
80
+
81
+ # Scaled backward pass
82
+ scaler.scale(loss).backward()
83
+
84
+ # Gradient clipping (before unscaling)
85
+ scaler.unscale_(optimizer)
86
+ torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
87
+
88
+ # Optimizer step
89
+ scaler.step(optimizer)
90
+ scaler.update()
91
+
92
+ total_loss += loss.item()
93
+ n_batches += 1
94
+
95
+ return {"loss": total_loss / n_batches}
96
+ ```
97
+
98
+ **Critical training loop rules**:
99
+ 1. `model.train()` before training, `model.eval()` before evaluation — these affect BatchNorm and Dropout
100
+ 2. `optimizer.zero_grad()` at the start of each batch, not the end
101
+ 3. Clip gradients before the optimizer step
102
+ 4. Use `loss.item()` (not `loss`) when accumulating — `.item()` detaches from the computation graph
103
+
104
+ ### Mixed Precision Training
105
+
106
+ Mixed precision (float16/bfloat16 for computation, float32 for parameters) typically provides 2–3x speedup on modern GPUs with minimal accuracy impact:
107
+
108
+ ```python
109
+ # Setup
110
+ scaler = torch.cuda.amp.GradScaler()
111
+
112
+ # Training step (shown above)
113
+ with torch.autocast(device_type="cuda", dtype=torch.float16):
114
+ outputs = model(inputs)
115
+ loss = criterion(outputs, targets)
116
+
117
+ scaler.scale(loss).backward()
118
+ scaler.unscale_(optimizer)
119
+ torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
120
+ scaler.step(optimizer)
121
+ scaler.update()
122
+ ```
123
+
124
+ **bfloat16 vs float16**:
125
+ - `bfloat16`: Same dynamic range as float32, less precision. Better for training stability. Requires Ampere GPU (A100, A30, RTX 30xx) or newer.
126
+ - `float16`: Better precision than bfloat16 but narrower dynamic range (overflow risk). Works on all CUDA GPUs.
127
+ - Default to `bfloat16` on Ampere+, `float16` on older GPUs.
128
+
129
+ ### Checkpointing
130
+
131
+ Save and restore training state completely — not just model weights:
132
+
133
+ ```python
134
+ def save_checkpoint(
135
+ path: str,
136
+ model: nn.Module,
137
+ optimizer: torch.optim.Optimizer,
138
+ scheduler,
139
+ scaler: torch.cuda.amp.GradScaler,
140
+ epoch: int,
141
+ metrics: dict,
142
+ ) -> None:
143
+ torch.save({
144
+ "epoch": epoch,
145
+ "model_state_dict": model.state_dict(),
146
+ "optimizer_state_dict": optimizer.state_dict(),
147
+ "scheduler_state_dict": scheduler.state_dict(),
148
+ "scaler_state_dict": scaler.state_dict(),
149
+ "metrics": metrics,
150
+ }, path)
151
+
152
+ def load_checkpoint(path: str, model, optimizer, scheduler, scaler):
153
+ checkpoint = torch.load(path, map_location="cpu")
154
+ model.load_state_dict(checkpoint["model_state_dict"])
155
+ optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
156
+ scheduler.load_state_dict(checkpoint["scheduler_state_dict"])
157
+ scaler.load_state_dict(checkpoint["scaler_state_dict"])
158
+ return checkpoint["epoch"], checkpoint["metrics"]
159
+ ```
160
+
161
+ **Checkpoint strategy**:
162
+ - Save every N epochs AND on best validation metric (two separate files)
163
+ - Keep last K checkpoints (delete older ones to save disk)
164
+ - Always test checkpoint resume — bugs in resume code are discovered in production during long training runs, not in testing
165
+
166
+ ### Distributed Training: DDP
167
+
168
+ PyTorch DistributedDataParallel (DDP) is the standard for multi-GPU training. Each GPU runs an independent process with a full model copy; gradients are averaged across GPUs after each backward pass:
169
+
170
+ ```python
171
+ # Launch: torchrun --nproc_per_node=4 train.py
172
+ import torch.distributed as dist
173
+ from torch.nn.parallel import DistributedDataParallel as DDP
174
+ from torch.utils.data.distributed import DistributedSampler
175
+
176
+ def train_distributed():
177
+ dist.init_process_group(backend="nccl")
178
+ rank = dist.get_rank()
179
+ local_rank = int(os.environ["LOCAL_RANK"])
180
+ world_size = dist.get_world_size()
181
+
182
+ device = torch.device(f"cuda:{local_rank}")
183
+ torch.cuda.set_device(device)
184
+
185
+ model = MyModel().to(device)
186
+ model = DDP(model, device_ids=[local_rank])
187
+
188
+ # Each rank gets a different data partition
189
+ sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
190
+ loader = DataLoader(dataset, sampler=sampler, batch_size=batch_size_per_gpu)
191
+
192
+ for epoch in range(epochs):
193
+ sampler.set_epoch(epoch) # Required for shuffle to work correctly
194
+ train_epoch(model, loader, ...)
195
+
196
+ # Save only from rank 0
197
+ if rank == 0:
198
+ torch.save(model.module.state_dict(), "model.pt") # .module unwraps DDP
199
+
200
+ dist.destroy_process_group()
201
+ ```
202
+
203
+ **DDP best practices**:
204
+ - Use `torchrun` (not `torch.multiprocessing.spawn`) for launch
205
+ - Effective batch size = `batch_size_per_gpu × world_size` — scale learning rate accordingly (linear scaling rule)
206
+ - Always call `sampler.set_epoch(epoch)` or shuffle is deterministic across epochs
207
+ - Log and save only from rank 0
208
+
209
+ ### Distributed Training: FSDP
210
+
211
+ Fully Sharded Data Parallel (FSDP) shards model parameters across GPUs, enabling training of models too large for a single GPU:
212
+
213
+ ```python
214
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
215
+ from torch.distributed.fsdp import MixedPrecision
216
+ import torch
217
+
218
+ bf16_policy = MixedPrecision(
219
+ param_dtype=torch.bfloat16,
220
+ reduce_dtype=torch.bfloat16,
221
+ buffer_dtype=torch.bfloat16,
222
+ )
223
+
224
+ model = FSDP(
225
+ model,
226
+ mixed_precision=bf16_policy,
227
+ auto_wrap_policy=transformer_auto_wrap_policy, # Wrap each transformer layer
228
+ )
229
+ ```
230
+
231
+ Use FSDP when model parameters exceed single-GPU memory. Use DDP when the model fits on one GPU — DDP is simpler and has less communication overhead.
232
+
233
+ ### Hyperparameter Tuning
234
+
235
+ Never tune hyperparameters manually at scale. Use a systematic search tool:
236
+
237
+ **Optuna** (open source, flexible):
238
+ ```python
239
+ import optuna
240
+
241
+ def objective(trial: optuna.Trial) -> float:
242
+ lr = trial.suggest_float("lr", 1e-5, 1e-2, log=True)
243
+ batch_size = trial.suggest_categorical("batch_size", [16, 32, 64])
244
+ dropout = trial.suggest_float("dropout", 0.0, 0.5)
245
+
246
+ model = build_model(dropout=dropout)
247
+ val_loss = train_and_evaluate(model, lr=lr, batch_size=batch_size)
248
+ return val_loss # Optuna minimises by default
249
+
250
+ study = optuna.create_study(direction="minimize", sampler=optuna.samplers.TPESampler())
251
+ study.optimize(objective, n_trials=50, n_jobs=4)
252
+
253
+ print(f"Best params: {study.best_params}")
254
+ print(f"Best value: {study.best_value:.4f}")
255
+ ```
256
+
257
+ **Key hyperparameters to tune** (in order of impact):
258
+ 1. Learning rate (most impactful — always tune first)
259
+ 2. Batch size (affects generalisation and training speed)
260
+ 3. Architecture (model size, depth, width)
261
+ 4. Regularisation (dropout, weight decay)
262
+ 5. Learning rate schedule (warmup steps, decay type)
263
+
264
+ **Search strategies**:
265
+ - Random search: Surprisingly effective, easy to parallelise
266
+ - Bayesian optimisation (TPE in Optuna): More efficient for small budgets
267
+ - Grid search: Only for 1–2 hyperparameters with small ranges
268
+
269
+ Report the best result with multiple seeds (mean ± std) — a single seed result may be a lucky draw.
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: mobile-app-architecture
3
+ description: MVVM/MVI/TCA patterns, navigation architecture, dependency injection, and state management for iOS and Android mobile apps
4
+ topics: [mobile-app, architecture, mvvm, mvi, tca, navigation, dependency-injection, state-management]
5
+ ---
6
+
7
+ Mobile app architecture determines testability, scalability, and developer velocity. The wrong architecture is expensive to reverse — a monolithic ViewController or God Activity becomes unmaintainable at scale. Both iOS and Android ecosystems have converged on unidirectional data flow patterns: TCA and MVVM+Combine/async for iOS, MVI and MVVM+Flow for Android. Choose the pattern that matches your team's size and complexity requirements, not the most sophisticated available option.
8
+
9
+ ## Summary
10
+
11
+ iOS architectures: MVVM with SwiftUI/Combine for mid-size apps, TCA (The Composable Architecture) for large apps requiring strict testability and state isolation. Android architectures: MVVM with StateFlow for most apps, MVI for complex state management. Both platforms benefit from clean architecture layers — presentation, domain, data — with dependency injection (Hilt for Android, constructor injection or a container for iOS). Navigation architecture is separate from view architecture: use Coordinator (iOS) or Navigation Component (Android).
12
+
13
+ ## Deep Guidance
14
+
15
+ ### iOS Architecture Patterns
16
+
17
+ **MVVM with SwiftUI**
18
+
19
+ The standard pattern for new iOS apps. The ViewModel is an `@Observable` class (iOS 17+) or `ObservableObject` (iOS 13+) that holds and transforms state:
20
+
21
+ ```swift
22
+ @Observable
23
+ final class UserProfileViewModel {
24
+ var user: User?
25
+ var isLoading = false
26
+ var error: Error?
27
+
28
+ private let repository: UserRepository
29
+
30
+ init(repository: UserRepository) {
31
+ self.repository = repository
32
+ }
33
+
34
+ func loadUser(id: String) async {
35
+ isLoading = true
36
+ defer { isLoading = false }
37
+ do {
38
+ user = try await repository.fetchUser(id: id)
39
+ } catch {
40
+ self.error = error
41
+ }
42
+ }
43
+ }
44
+
45
+ struct UserProfileView: View {
46
+ @State private var viewModel = UserProfileViewModel(repository: LiveUserRepository())
47
+
48
+ var body: some View {
49
+ Group {
50
+ if viewModel.isLoading { ProgressView() }
51
+ else if let user = viewModel.user { UserDetailView(user: user) }
52
+ else if viewModel.error != nil { ErrorView() }
53
+ }
54
+ .task { await viewModel.loadUser(id: userId) }
55
+ }
56
+ }
57
+ ```
58
+
59
+ Rules for healthy MVVM:
60
+ - ViewModels must not import UIKit or SwiftUI — they are platform-agnostic
61
+ - One ViewModel per screen/feature, not per view hierarchy level
62
+ - ViewModels receive dependencies via constructor injection — no singletons
63
+ - ViewModels hold only UI state, not business logic — business logic belongs in services/repositories
64
+ - Test ViewModels by injecting fake dependencies and asserting state transitions
65
+
66
+ **TCA (The Composable Architecture)**
67
+
68
+ For large apps with complex state, strict testability requirements, or large teams. TCA provides a single-direction state mutation model:
69
+
70
+ ```swift
71
+ @Reducer
72
+ struct UserProfileFeature {
73
+ @ObservableState
74
+ struct State: Equatable {
75
+ var user: User?
76
+ var isLoading = false
77
+ var error: String?
78
+ }
79
+
80
+ enum Action {
81
+ case loadUser(String)
82
+ case userLoaded(Result<User, Error>)
83
+ }
84
+
85
+ @Dependency(\.userRepository) var userRepository
86
+
87
+ var body: some ReducerOf<Self> {
88
+ Reduce { state, action in
89
+ switch action {
90
+ case .loadUser(let id):
91
+ state.isLoading = true
92
+ return .run { send in
93
+ await send(.userLoaded(Result { try await userRepository.fetchUser(id: id) }))
94
+ }
95
+ case .userLoaded(.success(let user)):
96
+ state.isLoading = false
97
+ state.user = user
98
+ return .none
99
+ case .userLoaded(.failure(let error)):
100
+ state.isLoading = false
101
+ state.error = error.localizedDescription
102
+ return .none
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ TCA benefits: every state mutation is explicit, side effects are isolated and cancellable, testing is deterministic. TCA costs: steep learning curve, boilerplate-heavy for simple features, requires team-wide adoption to be consistent.
110
+
111
+ **Clean Architecture layers for iOS**
112
+ ```
113
+ Presentation Layer: Views + ViewModels
114
+ ↓ calls
115
+ Domain Layer: Use Cases + Domain Models + Repository Protocols
116
+ ↓ calls
117
+ Data Layer: Repository Implementations + Network + Persistence
118
+ ```
119
+
120
+ - Domain layer has zero dependencies on UIKit, SwiftUI, or any specific framework
121
+ - Repository protocols defined in the domain layer, implemented in the data layer
122
+ - Use cases encapsulate single business operations: `FetchUserProfileUseCase`, `SubmitOrderUseCase`
123
+
124
+ ### Android Architecture Patterns
125
+
126
+ **MVVM with StateFlow**
127
+
128
+ The Google-recommended pattern, aligning with Android's official architecture guidance:
129
+
130
+ ```kotlin
131
+ data class UserProfileUiState(
132
+ val user: User? = null,
133
+ val isLoading: Boolean = false,
134
+ val error: String? = null
135
+ )
136
+
137
+ @HiltViewModel
138
+ class UserProfileViewModel @Inject constructor(
139
+ private val userRepository: UserRepository
140
+ ) : ViewModel() {
141
+
142
+ private val _uiState = MutableStateFlow(UserProfileUiState())
143
+ val uiState: StateFlow<UserProfileUiState> = _uiState.asStateFlow()
144
+
145
+ fun loadUser(userId: String) {
146
+ viewModelScope.launch {
147
+ _uiState.update { it.copy(isLoading = true) }
148
+ userRepository.fetchUser(userId)
149
+ .onSuccess { user ->
150
+ _uiState.update { it.copy(user = user, isLoading = false) }
151
+ }
152
+ .onFailure { error ->
153
+ _uiState.update { it.copy(error = error.message, isLoading = false) }
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ @Composable
160
+ fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
161
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
162
+ // render based on uiState
163
+ }
164
+ ```
165
+
166
+ **MVI (Model-View-Intent)**
167
+
168
+ For features with complex state machines where MVVM state updates become hard to reason about:
169
+
170
+ ```kotlin
171
+ sealed class UserProfileIntent {
172
+ data class LoadUser(val userId: String) : UserProfileIntent()
173
+ data object Refresh : UserProfileIntent()
174
+ }
175
+
176
+ sealed class UserProfileEffect {
177
+ data class ShowError(val message: String) : UserProfileEffect()
178
+ data object NavigateToLogin : UserProfileEffect()
179
+ }
180
+ ```
181
+
182
+ MVI separates user intentions from state mutations. The `SharedFlow` channel (`_effect`) handles one-shot events (navigation, toasts) that must not survive recomposition — a critical distinction from `StateFlow`.
183
+
184
+ **One-shot events vs. state**
185
+ - Use `StateFlow` for persistent UI state: loading, data, errors that survive recomposition
186
+ - Use `SharedFlow` or `Channel` (as `Flow`) for one-shot effects: navigation commands, snackbar messages, dialog triggers
187
+ - Never put navigation events in `StateFlow` — they replay on configuration change, causing double navigation
188
+
189
+ **Clean Architecture layers for Android**
190
+ ```
191
+ UI Layer: Composables + ViewModels
192
+ ↓ calls
193
+ Domain Layer: Use Cases + Domain Models + Repository Interfaces
194
+ ↓ calls
195
+ Data Layer: Repository Implementations + RemoteDataSource + LocalDataSource
196
+ ```
197
+
198
+ - Domain layer: pure Kotlin module (`core/domain`) with no Android dependencies
199
+ - Use cases: single `operator fun invoke()` or `execute()` function
200
+ - Repository pattern: the domain defines the interface; data layer implements it
201
+
202
+ ### Dependency Injection
203
+
204
+ **Android: Hilt**
205
+
206
+ Hilt is the recommended DI framework for Android:
207
+
208
+ ```kotlin
209
+ // Define a module
210
+ @Module
211
+ @InstallIn(SingletonComponent::class)
212
+ object NetworkModule {
213
+ @Provides
214
+ @Singleton
215
+ fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
216
+
217
+ @Provides
218
+ @Singleton
219
+ fun provideRetrofit(client: OkHttpClient): Retrofit = Retrofit.Builder()
220
+ .baseUrl(BuildConfig.BASE_URL)
221
+ .client(client)
222
+ .addConverterFactory(GsonConverterFactory.create())
223
+ .build()
224
+ }
225
+
226
+ // Inject into ViewModel
227
+ @HiltViewModel
228
+ class HomeViewModel @Inject constructor(
229
+ private val userRepository: UserRepository,
230
+ private val analyticsService: AnalyticsService
231
+ ) : ViewModel()
232
+ ```
233
+
234
+ Hilt scopes: `SingletonComponent` (app lifetime), `ActivityRetainedComponent` (ViewModel lifetime), `ViewModelComponent` (ViewModel scope), `FragmentComponent`, `ActivityComponent`. Match the scope to the dependency's actual lifetime.
235
+
236
+ **iOS: Constructor injection + container**
237
+
238
+ Swift does not have a dominant DI framework. Use constructor injection as the default:
239
+
240
+ ```swift
241
+ // Dependency container
242
+ final class AppDependencies {
243
+ static let shared = AppDependencies()
244
+
245
+ lazy var networkClient: NetworkClient = URLSessionNetworkClient()
246
+ lazy var userRepository: UserRepository = NetworkUserRepository(client: networkClient)
247
+ lazy var analyticsService: AnalyticsService = FirebaseAnalyticsService()
248
+ }
249
+
250
+ // Inject at the composition root (app entry point or Coordinator)
251
+ let viewModel = UserProfileViewModel(
252
+ repository: AppDependencies.shared.userRepository
253
+ )
254
+ ```
255
+
256
+ For testing: define protocols for all dependencies and inject fakes in tests. Never call `AppDependencies.shared` inside a ViewModel — inject via constructor.
257
+
258
+ ### State Management
259
+
260
+ **iOS state scoping**
261
+ - `@State`: view-local ephemeral state (animation flags, text field values) — does not survive view destruction
262
+ - `@Binding`: two-way binding from parent to child — child can mutate parent's state
263
+ - `@Observable` / `@ObservableObject`: shared mutable state in a ViewModel — survives view re-renders
264
+ - `@Environment`: dependency injection through the view tree (theme, locale, custom services)
265
+ - `@EnvironmentObject`: globally shared state accessed without explicit passing — use sparingly, only for truly app-wide state (user session, theme)
266
+ - Avoid prop-drilling state through 4+ view layers — use `@Environment` or restructure to lift state to a shared ancestor ViewModel
267
+
268
+ **Android state scoping**
269
+ - `remember { }`: view-local ephemeral state in Compose — survives recomposition, not configuration change
270
+ - `rememberSaveable { }`: survives configuration change by saving to Bundle
271
+ - `StateFlow` in ViewModel: survives configuration change automatically (ViewModel lifecycle)
272
+ - `SavedStateHandle` in ViewModel: persists through process death for critical state (form data, scroll position)
273
+ - Hoist state to the lowest ancestor that needs it — do not hoist everything to the ViewModel
274
+
275
+ **Handling configuration changes (Android)**
276
+ - ViewModel automatically survives rotation and theme change — the primary benefit of the ViewModel
277
+ - Always collect `StateFlow` with `collectAsStateWithLifecycle()` in Compose — stops collection when UI is not visible, preventing wasted work and crashes in background
278
+
279
+ **Background state handling (iOS)**
280
+ - ScenePhase: observe `\.scenePhase` in SwiftUI to react to foreground/background transitions
281
+ - Save in-progress work when entering background: `@Environment(\.scenePhase) var scenePhase`
282
+ - `@AppStorage`: lightweight persistence backed by UserDefaults for small values
283
+ - Combine state from multiple sources with `Publishers.CombineLatest` or async/await TaskGroup