@zigrivers/scaffold 3.7.0 → 3.8.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/README.md +43 -3
- package/content/knowledge/library/library-api-design.md +306 -0
- package/content/knowledge/library/library-architecture.md +247 -0
- package/content/knowledge/library/library-bundling.md +244 -0
- package/content/knowledge/library/library-conventions.md +229 -0
- package/content/knowledge/library/library-dev-environment.md +220 -0
- package/content/knowledge/library/library-documentation.md +300 -0
- package/content/knowledge/library/library-project-structure.md +237 -0
- package/content/knowledge/library/library-requirements.md +173 -0
- package/content/knowledge/library/library-security.md +257 -0
- package/content/knowledge/library/library-testing.md +319 -0
- package/content/knowledge/library/library-type-definitions.md +284 -0
- package/content/knowledge/library/library-versioning.md +300 -0
- package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
- package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
- package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
- package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
- package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
- package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
- package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
- package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
- package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
- package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
- package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
- package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
- package/content/methodology/library-overlay.yml +67 -0
- package/content/methodology/mobile-app-overlay.yml +71 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +82 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +70 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/config/schema.d.ts +592 -32
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +34 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +147 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +22 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.d.ts +2 -1
- package/dist/e2e/project-type-overlays.test.d.ts.map +1 -1
- package/dist/e2e/project-type-overlays.test.js +302 -2
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/types/config.d.ts +7 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/wizard/questions.d.ts +12 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +56 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +89 -4
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +9 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +12 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-versioning
|
|
3
|
+
description: Semver discipline, breaking change detection, release automation, and changelog management for published libraries
|
|
4
|
+
topics: [library, versioning, semver, breaking-changes, release-automation, changelog, changesets]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Library versioning is a communication protocol with consumers. Semver (Semantic Versioning) is not merely a numbering scheme — it is a contract about backward compatibility. Breaking that contract without a major version bump is one of the most damaging things a library can do. Consumers set version ranges expecting that minor updates are safe to take automatically. Violating that expectation causes production incidents for real applications. Versioning discipline must be enforced by tooling, not willpower.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Enforce semver through tooling: use changesets or semantic-release to automate versioning based on change metadata. Use automated breaking change detection (API Extractor or type-coverage checks) to catch accidental breaking changes before publish. Every release requires a CHANGELOG entry with migration guidance for breaking changes. Pre-releases (`alpha`, `beta`, `rc`) allow consumers to opt into early testing without affecting stable installs. Tag releases in git to enable diff-based changelog generation.
|
|
12
|
+
|
|
13
|
+
Versioning workflow:
|
|
14
|
+
1. Author creates changeset file describing the change type (patch/minor/major)
|
|
15
|
+
2. CI aggregates changesets and proposes a version bump PR
|
|
16
|
+
3. Version bump PR merges, triggering publish to npm
|
|
17
|
+
4. Git tag pushed matching the published version
|
|
18
|
+
5. GitHub Release created from changelog content
|
|
19
|
+
|
|
20
|
+
## Deep Guidance
|
|
21
|
+
|
|
22
|
+
### Changesets Workflow
|
|
23
|
+
|
|
24
|
+
Changesets is the recommended tool for managing versioning in library projects. It decouples the decision of "what version bump does this change require" from "when do we publish."
|
|
25
|
+
|
|
26
|
+
**Setup:**
|
|
27
|
+
```bash
|
|
28
|
+
npm install --save-dev @changesets/cli
|
|
29
|
+
npx changeset init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates a `.changeset/` directory at the project root.
|
|
33
|
+
|
|
34
|
+
**Creating a changeset (run for every PR that changes behavior):**
|
|
35
|
+
```bash
|
|
36
|
+
npx changeset add
|
|
37
|
+
# Interactive prompt:
|
|
38
|
+
# ? Which packages would you like to include? my-library
|
|
39
|
+
# ? What type of change is this for my-library?
|
|
40
|
+
# major (Breaking change)
|
|
41
|
+
# minor (New feature)
|
|
42
|
+
# > patch (Bug fix)
|
|
43
|
+
# ? Please enter a summary for this change:
|
|
44
|
+
# Fix parseConfig() incorrectly ignoring the encoding option
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This creates a markdown file in `.changeset/`:
|
|
48
|
+
```markdown
|
|
49
|
+
<!-- .changeset/silver-wolves-grin.md -->
|
|
50
|
+
---
|
|
51
|
+
"my-library": patch
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
Fix parseConfig() incorrectly ignoring the encoding option when parsing file input.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Version bump and publish:**
|
|
58
|
+
```bash
|
|
59
|
+
# Update package.json version and CHANGELOG.md
|
|
60
|
+
npx changeset version
|
|
61
|
+
|
|
62
|
+
# Publish to npm
|
|
63
|
+
npx changeset publish
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
In CI, automate this with the Changesets GitHub Action:
|
|
67
|
+
```yaml
|
|
68
|
+
# .github/workflows/release.yml
|
|
69
|
+
# name: Release — CI publishes on push to main
|
|
70
|
+
on:
|
|
71
|
+
push:
|
|
72
|
+
branches: [main]
|
|
73
|
+
|
|
74
|
+
jobs:
|
|
75
|
+
release:
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
- uses: actions/setup-node@v4
|
|
80
|
+
with:
|
|
81
|
+
node-version: 20
|
|
82
|
+
registry-url: 'https://registry.npmjs.org'
|
|
83
|
+
- run: npm ci
|
|
84
|
+
- uses: changesets/action@v1
|
|
85
|
+
with:
|
|
86
|
+
publish: npm run release
|
|
87
|
+
env:
|
|
88
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
89
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The action opens a "Version Packages" PR when changesets are present and publishes when that PR merges.
|
|
93
|
+
|
|
94
|
+
### Breaking Change Detection with API Extractor
|
|
95
|
+
|
|
96
|
+
Microsoft's API Extractor catches breaking changes by comparing the current API surface against a committed baseline:
|
|
97
|
+
|
|
98
|
+
**Setup:**
|
|
99
|
+
```bash
|
|
100
|
+
npm install --save-dev @microsoft/api-extractor
|
|
101
|
+
npx api-extractor init
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
// api-extractor.json (key settings)
|
|
106
|
+
{
|
|
107
|
+
"mainEntryPointFilePath": "<projectFolder>/dist/types/index.d.ts",
|
|
108
|
+
"apiReport": {
|
|
109
|
+
"enabled": true,
|
|
110
|
+
"reportFolder": "<projectFolder>/etc/"
|
|
111
|
+
},
|
|
112
|
+
"docModel": {
|
|
113
|
+
"enabled": true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Generate initial API report (commit this file)
|
|
120
|
+
npx api-extractor run --local
|
|
121
|
+
|
|
122
|
+
# In CI: compare against committed report
|
|
123
|
+
npx api-extractor run
|
|
124
|
+
# Fails if the API surface changed in ways not reflected in the committed report
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The generated `etc/my-library.api.md` file shows the complete public API surface in a reviewable format. When a PR changes it, reviewers can see exactly what changed. If the change is intentional, update the committed report; if not, fix the breaking change.
|
|
128
|
+
|
|
129
|
+
**API report excerpt:**
|
|
130
|
+
```markdown
|
|
131
|
+
// @public
|
|
132
|
+
export function parseConfig(input: string, options?: ParseOptions): Config;
|
|
133
|
+
|
|
134
|
+
// @public
|
|
135
|
+
export interface ParseOptions {
|
|
136
|
+
encoding?: BufferEncoding;
|
|
137
|
+
strict?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// @public
|
|
141
|
+
export class ParseError extends Error {
|
|
142
|
+
constructor(message: string, line: number, column: number);
|
|
143
|
+
readonly column: number;
|
|
144
|
+
readonly line: number;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This format makes breaking changes immediately visible in code review.
|
|
149
|
+
|
|
150
|
+
### Pre-Release Channels
|
|
151
|
+
|
|
152
|
+
Pre-releases allow consumers to test upcoming changes without affecting stable installs:
|
|
153
|
+
|
|
154
|
+
**With changesets:**
|
|
155
|
+
```bash
|
|
156
|
+
# Enter pre-release mode
|
|
157
|
+
npx changeset pre enter alpha
|
|
158
|
+
# Or: beta, rc
|
|
159
|
+
|
|
160
|
+
# Create changesets and version as normal
|
|
161
|
+
npx changeset add
|
|
162
|
+
npx changeset version
|
|
163
|
+
# Produces: 2.0.0-alpha.1
|
|
164
|
+
|
|
165
|
+
# Exit pre-release mode
|
|
166
|
+
npx changeset pre exit
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Manual pre-release versioning:**
|
|
170
|
+
```json
|
|
171
|
+
// package.json
|
|
172
|
+
"version": "2.0.0-alpha.1"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npm publish --tag alpha
|
|
177
|
+
# Consumers opt-in: npm install my-library@alpha
|
|
178
|
+
# Stable consumers (npm install my-library) are unaffected
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Pre-release channel strategy:**
|
|
182
|
+
- `alpha` — internal testing only, may change drastically, no API stability
|
|
183
|
+
- `beta` — public testing, API reasonably stable, looking for feedback
|
|
184
|
+
- `rc` (release candidate) — API frozen, looking for final integration issues
|
|
185
|
+
- Stable — semver protected, change policy enforced
|
|
186
|
+
|
|
187
|
+
### Release Automation with GitHub Actions
|
|
188
|
+
|
|
189
|
+
Full release workflow with provenance and attestation:
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
# .github/workflows/release.yml
|
|
193
|
+
# name: Release — CI publishes on tag push
|
|
194
|
+
on:
|
|
195
|
+
push:
|
|
196
|
+
tags:
|
|
197
|
+
- 'v*'
|
|
198
|
+
|
|
199
|
+
permissions:
|
|
200
|
+
contents: write
|
|
201
|
+
id-token: write # For npm provenance
|
|
202
|
+
|
|
203
|
+
jobs:
|
|
204
|
+
release:
|
|
205
|
+
runs-on: ubuntu-latest
|
|
206
|
+
steps:
|
|
207
|
+
- uses: actions/checkout@v4
|
|
208
|
+
|
|
209
|
+
- uses: actions/setup-node@v4
|
|
210
|
+
with:
|
|
211
|
+
node-version: '20'
|
|
212
|
+
registry-url: 'https://registry.npmjs.org'
|
|
213
|
+
|
|
214
|
+
- name: Install dependencies
|
|
215
|
+
run: npm ci
|
|
216
|
+
|
|
217
|
+
- name: Build
|
|
218
|
+
run: npm run build
|
|
219
|
+
|
|
220
|
+
- name: Test
|
|
221
|
+
run: npm test
|
|
222
|
+
|
|
223
|
+
- name: Publish to npm
|
|
224
|
+
run: npm publish --provenance --access public
|
|
225
|
+
env:
|
|
226
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
227
|
+
|
|
228
|
+
- name: Create GitHub Release
|
|
229
|
+
uses: softprops/action-gh-release@v2
|
|
230
|
+
with:
|
|
231
|
+
generate_release_notes: true
|
|
232
|
+
draft: false
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The `--provenance` flag publishes npm provenance attestation — a cryptographic link between the published package and the GitHub Actions run that built it. This allows consumers to verify the package was built from the expected source.
|
|
236
|
+
|
|
237
|
+
### CHANGELOG Generation
|
|
238
|
+
|
|
239
|
+
Keep a Changelog format, managed automatically:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# With conventional commits, generate changelog automatically:
|
|
243
|
+
npx conventional-changelog-cli -p angular -i CHANGELOG.md -s
|
|
244
|
+
|
|
245
|
+
# Or with changesets:
|
|
246
|
+
npx changeset version # Updates CHANGELOG.md automatically
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Manual CHANGELOG structure:**
|
|
250
|
+
```markdown
|
|
251
|
+
# Changelog
|
|
252
|
+
|
|
253
|
+
All notable changes to this project will be documented in this file.
|
|
254
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
255
|
+
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
256
|
+
|
|
257
|
+
## [Unreleased]
|
|
258
|
+
|
|
259
|
+
## [2.1.0] - 2024-03-15
|
|
260
|
+
|
|
261
|
+
### Added
|
|
262
|
+
- `parseConfigFile(path)` for file-based parsing
|
|
263
|
+
- `ParseOptions.maxSize` to limit input size
|
|
264
|
+
|
|
265
|
+
### Fixed
|
|
266
|
+
- `parseConfig()` no longer ignores `encoding` option for buffer inputs
|
|
267
|
+
|
|
268
|
+
## [2.0.0] - 2024-01-10
|
|
269
|
+
|
|
270
|
+
### Breaking Changes
|
|
271
|
+
- Removed `parse()` (deprecated in 1.5.0). Replacement: `parseConfig()`.
|
|
272
|
+
- `Config.timeout` is now milliseconds (was seconds). Multiply existing values × 1000.
|
|
273
|
+
- Dropped Node 16 support. Minimum: Node 18.
|
|
274
|
+
|
|
275
|
+
### Migration Guide
|
|
276
|
+
See: https://my-library.dev/guides/migration-v2
|
|
277
|
+
|
|
278
|
+
[Unreleased]: https://github.com/org/my-library/compare/v2.1.0...HEAD
|
|
279
|
+
[2.1.0]: https://github.com/org/my-library/compare/v2.0.0...v2.1.0
|
|
280
|
+
[2.0.0]: https://github.com/org/my-library/releases/tag/v2.0.0
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Git Tag Strategy
|
|
284
|
+
|
|
285
|
+
Tag every release:
|
|
286
|
+
```bash
|
|
287
|
+
# After publishing to npm
|
|
288
|
+
git tag v2.1.0
|
|
289
|
+
git push origin v2.1.0
|
|
290
|
+
|
|
291
|
+
# Or use npm version (updates package.json, commits, and tags)
|
|
292
|
+
npm version minor -m "chore(release): v%s"
|
|
293
|
+
git push && git push --tags
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Tags are the source of truth for "what was published when." They enable:
|
|
297
|
+
- Reproducible builds from any historical version
|
|
298
|
+
- `git diff v2.0.0 v2.1.0` to review what changed between releases
|
|
299
|
+
- Automated changelog generation tools
|
|
300
|
+
- GitHub Release creation linked to the exact commit
|
|
@@ -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
|