@haposoft/cafekit 0.16.0-rc.8 → 0.16.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 +116 -27
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +50 -0
- package/bin/__tests__/codex-native.test.js +1882 -70
- package/bin/__tests__/develop-contract.test.js +400 -23
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/optional-skill-inventory.test.js +249 -0
- package/bin/__tests__/output-styles.test.js +214 -0
- package/bin/__tests__/package-inventory.test.js +1735 -98
- package/bin/__tests__/runtime-schema.test.js +111 -0
- package/bin/__tests__/secret-output-guardrail.test.js +122 -0
- package/bin/__tests__/skill-routing-source.test.js +172 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
- package/bin/install.js +10 -0
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +14 -0
- package/bin/lib/i18n.js +12 -6
- package/bin/lib/skill-deps.js +5 -5
- package/bin/phases/claude-runtime.js +23 -0
- package/bin/phases/claude-settings.js +8 -0
- package/bin/phases/codex-hooks.js +156 -0
- package/bin/phases/codex-runtime.js +29 -38
- package/bin/phases/copy-payload.js +8 -2
- package/bin/phases/select-skill-bundles.js +96 -0
- package/bin/phases/skill-inventory.js +71 -0
- package/bin/phases/skills-setup.js +9 -2
- package/bin/phases/write-metadata.js +8 -5
- package/package.json +8 -2
- package/src/claude/CLAUDE.md +3 -3
- package/src/claude/agents/brainstormer.md +52 -69
- package/src/claude/agents/code-auditor.md +32 -14
- package/src/claude/agents/debugger.md +35 -20
- package/src/claude/agents/deployer.md +12 -5
- package/src/claude/agents/docs-keeper.md +11 -1
- package/src/claude/agents/implementer.md +25 -13
- package/src/claude/agents/inspector.md +6 -3
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/researcher.md +26 -55
- package/src/claude/agents/spec-maker.md +12 -7
- package/src/claude/agents/strategist.md +52 -0
- package/src/claude/agents/test-runner.md +117 -132
- package/src/claude/agents/ui-ux-designer.md +1 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
- package/src/claude/hooks/__tests__/precompact.test.js +158 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +10 -3
- package/src/claude/hooks/__tests__/state.test.js +5 -0
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +1 -1
- package/src/claude/hooks/docs-sync.cjs +12 -10
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +3 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +105 -0
- package/src/claude/hooks/rules.cjs +1 -1
- package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
- package/src/claude/hooks/session.cjs +27 -1
- package/src/claude/hooks/spec-gate.cjs +3 -3
- package/src/claude/hooks/spec-state.cjs +2 -2
- package/src/claude/hooks/state.cjs +1 -1
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/hooks/usage.cjs +1 -1
- package/src/claude/migration-manifest.json +29 -15
- package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
- package/src/claude/output-styles/coding-level-1-junior.md +124 -0
- package/src/claude/output-styles/coding-level-2-mid.md +146 -0
- package/src/claude/output-styles/coding-level-3-senior.md +148 -0
- package/src/claude/output-styles/coding-level-4-lead.md +159 -0
- package/src/claude/output-styles/coding-level-5-god.md +91 -0
- package/src/claude/references/debugger/core-philosophy.md +5 -3
- package/src/claude/references/debugger/frontend-verification.md +2 -2
- package/src/claude/references/debugger/log-ci-analysis.md +13 -1
- package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
- package/src/claude/references/debugger/performance-diagnostics.md +2 -2
- package/src/claude/references/debugger/root-cause-tracing.md +37 -6
- package/src/claude/references/debugger/side-effect-gate.md +3 -1
- package/src/claude/references/debugger/verification-protocol.md +13 -12
- package/src/claude/rules/orchestrator.md +7 -1
- package/src/claude/rules/process-management.md +39 -0
- package/src/claude/rules/review-audit-self-decision.md +48 -0
- package/src/claude/rules/skill-domain-routing.md +26 -24
- package/src/claude/rules/skill-workflow-routing.md +22 -14
- package/src/claude/rules/workflow.md +3 -3
- package/src/claude/runtime.json +1 -0
- package/src/claude/runtime.schema.json +205 -0
- package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
- package/src/claude/scripts/provenance.cjs +18 -5
- package/src/claude/scripts/workflow-policy.cjs +1 -1
- package/src/claude/settings/settings.json +16 -0
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/brainstorm/SKILL.md +193 -172
- package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
- package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +43 -25
- package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
- package/src/claude/skills/code-review/references/verification-gate.md +1 -1
- package/src/claude/skills/debug/SKILL.md +48 -19
- package/src/claude/skills/delegate/SKILL.md +1 -1
- package/src/claude/skills/develop/SKILL.md +92 -65
- package/src/claude/skills/develop/references/parallel-waves.md +36 -63
- package/src/claude/skills/develop/references/quality-gate.md +31 -4
- package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
- package/src/claude/skills/docs/SKILL.md +42 -23
- package/src/claude/skills/docs/references/init-workflow.md +4 -4
- package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
- package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
- package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +9 -7
- package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
- package/src/claude/skills/docx/SKILL.md +1 -1
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/commit-protocols.md +1 -1
- package/src/claude/skills/git/references/finish-branch.md +1 -1
- package/src/claude/skills/hotfix/SKILL.md +159 -170
- package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
- package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
- package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
- package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
- package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
- package/src/claude/skills/inspect/SKILL.md +55 -46
- package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
- package/src/claude/skills/loop/SKILL.md +114 -0
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
- package/src/claude/skills/pdf/SKILL.md +1 -1
- package/src/claude/skills/pptx/SKILL.md +1 -1
- package/src/claude/skills/question/SKILL.md +16 -13
- package/src/claude/skills/research/SKILL.md +70 -43
- package/src/claude/skills/route/SKILL.md +94 -0
- package/src/claude/skills/route/references/agent-timing.md +46 -0
- package/src/claude/skills/route/references/chaining-patterns.md +53 -0
- package/src/claude/skills/route/references/task-taxonomy.md +48 -0
- package/src/claude/skills/specs/SKILL.md +41 -60
- package/src/claude/skills/specs/references/review.md +11 -8
- package/src/claude/skills/specs/references/templates.md +34 -18
- package/src/claude/skills/specs/templates/requirements-init.md +1 -1
- package/src/claude/skills/specs/templates/research.md +2 -2
- package/src/claude/skills/sync/SKILL.md +6 -6
- package/src/claude/skills/sync/references/sync-protocols.md +1 -1
- package/src/claude/skills/test/SKILL.md +120 -120
- package/src/claude/skills/test/references/execution-strategy.md +208 -361
- package/src/claude/skills/test/references/failure-triage.md +55 -121
- package/src/claude/skills/test/references/test-memory.md +20 -34
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/skills/web-testing/SKILL.md +1 -1
- package/src/claude/skills/xlsx/SKILL.md +1 -1
- package/src/claude/status.cjs +135 -33
- package/src/codex/AGENTS.md +14 -4
- package/src/codex/hooks/docs-sync.cjs +8 -3
- package/src/codex/hooks/lib/hook-context.cjs +18 -4
- package/src/codex/hooks/rules.cjs +29 -1
- package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
- package/src/codex/hooks/spec-gate.cjs +3 -8
- package/src/codex/hooks/spec-state.cjs +2 -1
- package/src/codex/hooks.json +5 -0
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/claude/skills/backend-development/SKILL.md +0 -101
- package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
- package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
- package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
- package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
- package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
- package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
- package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
- package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
- package/src/claude/skills/backend-development/references/backend-security.md +0 -342
- package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
- package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
- package/src/claude/skills/devops/.env.example +0 -76
- package/src/claude/skills/devops/SKILL.md +0 -102
- package/src/claude/skills/devops/references/browser-rendering.md +0 -305
- package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
- package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
- package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
- package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
- package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
- package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
- package/src/claude/skills/devops/references/docker-basics.md +0 -297
- package/src/claude/skills/devops/references/docker-compose.md +0 -292
- package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
- package/src/claude/skills/devops/references/gcloud-services.md +0 -304
- package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
- package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
- package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
- package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
- package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
- package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
- package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
- package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
- package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
- package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
- package/src/claude/skills/devops/scripts/requirements.txt +0 -20
- package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
- package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/src/claude/skills/frontend-design/SKILL.md +0 -135
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
- package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
- package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
- package/src/claude/skills/frontend-design/references/animejs.md +0 -396
- package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
- package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
- package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
- package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
- package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
- package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
- package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
- package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
- package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
- package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
- package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
- package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
- package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
- package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
- package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
- package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
- package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
- package/src/claude/skills/frontend-development/SKILL.md +0 -406
- package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
- package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
- package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
- package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
- package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
- package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
- package/src/claude/skills/frontend-development/resources/performance.md +0 -406
- package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
- package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
- package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
- package/src/claude/skills/mobile-development/SKILL.md +0 -218
- package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
- package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
- package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
- package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
- package/src/claude/skills/react-best-practices/README.md +0 -123
- package/src/claude/skills/react-best-practices/SKILL.md +0 -128
- package/src/claude/skills/react-best-practices/metadata.json +0 -15
- package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
- package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
- package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
- package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
- package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
- package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
- package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
- package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
- package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
- package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
- package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
- package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
- package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
- package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
|
@@ -1,604 +0,0 @@
|
|
|
1
|
-
# Android Native Development
|
|
2
|
-
|
|
3
|
-
Complete guide to Android development with Kotlin and Jetpack Compose (2024-2025).
|
|
4
|
-
|
|
5
|
-
## Kotlin 2.1 Overview
|
|
6
|
-
|
|
7
|
-
### Key Features
|
|
8
|
-
- **Null safety**: No more NullPointerExceptions
|
|
9
|
-
- **Coroutines**: Structured concurrency
|
|
10
|
-
- **Extension functions**: Extend classes without inheritance
|
|
11
|
-
- **Sealed classes**: Type-safe state management
|
|
12
|
-
- **Data classes**: Automatic equals/hashCode/toString
|
|
13
|
-
|
|
14
|
-
### Modern Kotlin Patterns
|
|
15
|
-
|
|
16
|
-
**Coroutines:**
|
|
17
|
-
```kotlin
|
|
18
|
-
// Suspend function
|
|
19
|
-
suspend fun fetchUser(id: String): User {
|
|
20
|
-
return withContext(Dispatchers.IO) {
|
|
21
|
-
api.getUser(id)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Usage in ViewModel
|
|
26
|
-
viewModelScope.launch {
|
|
27
|
-
try {
|
|
28
|
-
val user = fetchUser("123")
|
|
29
|
-
_uiState.update { it.copy(user = user) }
|
|
30
|
-
} catch (e: Exception) {
|
|
31
|
-
_uiState.update { it.copy(error = e.message) }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**Flow (Reactive streams):**
|
|
37
|
-
```kotlin
|
|
38
|
-
class UserRepository {
|
|
39
|
-
fun observeUsers(): Flow<List<User>> = flow {
|
|
40
|
-
while (true) {
|
|
41
|
-
emit(database.getUsers())
|
|
42
|
-
delay(5000) // Poll every 5 seconds
|
|
43
|
-
}
|
|
44
|
-
}.flowOn(Dispatchers.IO)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Collect in ViewModel
|
|
48
|
-
init {
|
|
49
|
-
viewModelScope.launch {
|
|
50
|
-
repository.observeUsers().collect { users ->
|
|
51
|
-
_uiState.update { it.copy(users = users) }
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**Sealed classes (Type-safe states):**
|
|
58
|
-
```kotlin
|
|
59
|
-
sealed class UiState {
|
|
60
|
-
object Loading : UiState()
|
|
61
|
-
data class Success(val data: List<User>) : UiState()
|
|
62
|
-
data class Error(val message: String) : UiState()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Pattern matching
|
|
66
|
-
when (uiState) {
|
|
67
|
-
is UiState.Loading -> ShowLoader()
|
|
68
|
-
is UiState.Success -> ShowData(uiState.data)
|
|
69
|
-
is UiState.Error -> ShowError(uiState.message)
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Jetpack Compose
|
|
74
|
-
|
|
75
|
-
### Why Compose?
|
|
76
|
-
- **Declarative**: Describe UI state, not imperative commands
|
|
77
|
-
- **Adoption**: Used in many top 1,000 apps
|
|
78
|
-
- **Less code**: Reduction vs Views
|
|
79
|
-
- **Modern**: Built for Kotlin and coroutines
|
|
80
|
-
- **Material 3**: First-class support
|
|
81
|
-
|
|
82
|
-
### Compose Basics
|
|
83
|
-
|
|
84
|
-
```kotlin
|
|
85
|
-
@Composable
|
|
86
|
-
fun UserListScreen(viewModel: UserViewModel = viewModel()) {
|
|
87
|
-
val uiState by viewModel.uiState.collectAsState()
|
|
88
|
-
|
|
89
|
-
Column(modifier = Modifier.fillMaxSize()) {
|
|
90
|
-
when (val state = uiState) {
|
|
91
|
-
is UiState.Loading -> {
|
|
92
|
-
CircularProgressIndicator(
|
|
93
|
-
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
is UiState.Success -> {
|
|
97
|
-
LazyColumn {
|
|
98
|
-
items(state.data) { user ->
|
|
99
|
-
UserItem(user)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
is UiState.Error -> {
|
|
104
|
-
Text(
|
|
105
|
-
text = state.message,
|
|
106
|
-
color = MaterialTheme.colorScheme.error
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@Composable
|
|
114
|
-
fun UserItem(user: User) {
|
|
115
|
-
Row(
|
|
116
|
-
modifier = Modifier
|
|
117
|
-
.fillMaxWidth()
|
|
118
|
-
.padding(16.dp)
|
|
119
|
-
) {
|
|
120
|
-
Text(
|
|
121
|
-
text = user.name,
|
|
122
|
-
style = MaterialTheme.typography.bodyLarge
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Key Composables:**
|
|
129
|
-
- `Column/Row/Box`: Layout
|
|
130
|
-
- `LazyColumn/LazyRow`: Recycler equivalent (virtualized)
|
|
131
|
-
- `Text/Image/Icon`: Content
|
|
132
|
-
- `Button/TextField`: Input
|
|
133
|
-
- `Card/Surface`: Containers
|
|
134
|
-
|
|
135
|
-
## Architecture Patterns
|
|
136
|
-
|
|
137
|
-
### MVVM with Clean Architecture
|
|
138
|
-
|
|
139
|
-
```kotlin
|
|
140
|
-
// Domain Layer - Use Case
|
|
141
|
-
class GetUsersUseCase @Inject constructor(
|
|
142
|
-
private val repository: UserRepository
|
|
143
|
-
) {
|
|
144
|
-
operator fun invoke(): Flow<Result<List<User>>> =
|
|
145
|
-
repository.getUsers()
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Data Layer - Repository
|
|
149
|
-
interface UserRepository {
|
|
150
|
-
fun getUsers(): Flow<Result<List<User>>>
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
class UserRepositoryImpl @Inject constructor(
|
|
154
|
-
private val api: UserApi,
|
|
155
|
-
private val dao: UserDao
|
|
156
|
-
) : UserRepository {
|
|
157
|
-
override fun getUsers(): Flow<Result<List<User>>> = flow {
|
|
158
|
-
// Local cache first
|
|
159
|
-
val cachedUsers = dao.getUsers()
|
|
160
|
-
emit(Result.success(cachedUsers))
|
|
161
|
-
|
|
162
|
-
// Then fetch from network
|
|
163
|
-
try {
|
|
164
|
-
val networkUsers = api.getUsers()
|
|
165
|
-
dao.insertUsers(networkUsers)
|
|
166
|
-
emit(Result.success(networkUsers))
|
|
167
|
-
} catch (e: Exception) {
|
|
168
|
-
emit(Result.failure(e))
|
|
169
|
-
}
|
|
170
|
-
}.flowOn(Dispatchers.IO)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Presentation Layer - ViewModel
|
|
174
|
-
@HiltViewModel
|
|
175
|
-
class UserViewModel @Inject constructor(
|
|
176
|
-
private val getUsersUseCase: GetUsersUseCase
|
|
177
|
-
) : ViewModel() {
|
|
178
|
-
|
|
179
|
-
private val _uiState = MutableStateFlow(UserUiState())
|
|
180
|
-
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
|
|
181
|
-
|
|
182
|
-
init {
|
|
183
|
-
loadUsers()
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private fun loadUsers() {
|
|
187
|
-
viewModelScope.launch {
|
|
188
|
-
getUsersUseCase().collect { result ->
|
|
189
|
-
result.onSuccess { users ->
|
|
190
|
-
_uiState.update { it.copy(users = users, isLoading = false) }
|
|
191
|
-
}.onFailure { error ->
|
|
192
|
-
_uiState.update { it.copy(error = error.message, isLoading = false) }
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// UI State
|
|
200
|
-
data class UserUiState(
|
|
201
|
-
val users: List<User> = emptyList(),
|
|
202
|
-
val isLoading: Boolean = true,
|
|
203
|
-
val error: String? = null
|
|
204
|
-
)
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### MVI (Model-View-Intent)
|
|
208
|
-
|
|
209
|
-
**When to use:**
|
|
210
|
-
- Unidirectional data flow needed
|
|
211
|
-
- Complex state management
|
|
212
|
-
- Time-travel debugging
|
|
213
|
-
- Predictable state updates
|
|
214
|
-
|
|
215
|
-
```kotlin
|
|
216
|
-
// State
|
|
217
|
-
data class UserScreenState(
|
|
218
|
-
val users: List<User> = emptyList(),
|
|
219
|
-
val isLoading: Boolean = false,
|
|
220
|
-
val error: String? = null
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
// Events (User intentions)
|
|
224
|
-
sealed class UserEvent {
|
|
225
|
-
object LoadUsers : UserEvent()
|
|
226
|
-
data class DeleteUser(val id: String) : UserEvent()
|
|
227
|
-
object RetryLoad : UserEvent()
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ViewModel
|
|
231
|
-
class UserViewModel : ViewModel() {
|
|
232
|
-
private val _state = MutableStateFlow(UserScreenState())
|
|
233
|
-
val state: StateFlow<UserScreenState> = _state.asStateFlow()
|
|
234
|
-
|
|
235
|
-
fun onEvent(event: UserEvent) {
|
|
236
|
-
when (event) {
|
|
237
|
-
is UserEvent.LoadUsers -> loadUsers()
|
|
238
|
-
is UserEvent.DeleteUser -> deleteUser(event.id)
|
|
239
|
-
is UserEvent.RetryLoad -> loadUsers()
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
## Dependency Injection
|
|
246
|
-
|
|
247
|
-
### Hilt (Recommended for Large Apps)
|
|
248
|
-
|
|
249
|
-
**Setup:**
|
|
250
|
-
```kotlin
|
|
251
|
-
// App class
|
|
252
|
-
@HiltAndroidApp
|
|
253
|
-
class MyApplication : Application()
|
|
254
|
-
|
|
255
|
-
// Activity
|
|
256
|
-
@AndroidEntryPoint
|
|
257
|
-
class MainActivity : ComponentActivity()
|
|
258
|
-
|
|
259
|
-
// ViewModel
|
|
260
|
-
@HiltViewModel
|
|
261
|
-
class UserViewModel @Inject constructor(
|
|
262
|
-
private val repository: UserRepository,
|
|
263
|
-
private val analytics: Analytics
|
|
264
|
-
) : ViewModel()
|
|
265
|
-
|
|
266
|
-
// Module
|
|
267
|
-
@Module
|
|
268
|
-
@InstallIn(SingletonComponent::class)
|
|
269
|
-
object NetworkModule {
|
|
270
|
-
@Provides
|
|
271
|
-
@Singleton
|
|
272
|
-
fun provideRetrofit(): Retrofit = Retrofit.Builder()
|
|
273
|
-
.baseUrl("https://api.example.com")
|
|
274
|
-
.addConverterFactory(GsonConverterFactory.create())
|
|
275
|
-
.build()
|
|
276
|
-
|
|
277
|
-
@Provides
|
|
278
|
-
@Singleton
|
|
279
|
-
fun provideUserApi(retrofit: Retrofit): UserApi =
|
|
280
|
-
retrofit.create(UserApi::class.java)
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Koin (Lightweight Alternative)
|
|
285
|
-
|
|
286
|
-
**Setup:**
|
|
287
|
-
```kotlin
|
|
288
|
-
// Module definition
|
|
289
|
-
val appModule = module {
|
|
290
|
-
single { UserRepository(get()) }
|
|
291
|
-
viewModel { UserViewModel(get()) }
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Application
|
|
295
|
-
class MyApp : Application() {
|
|
296
|
-
override fun onCreate() {
|
|
297
|
-
super.onCreate()
|
|
298
|
-
startKoin {
|
|
299
|
-
androidContext(this@MyApp)
|
|
300
|
-
modules(appModule)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Usage
|
|
306
|
-
class UserViewModel(
|
|
307
|
-
private val repository: UserRepository
|
|
308
|
-
) : ViewModel()
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
**Hilt vs Koin:**
|
|
312
|
-
- **Hilt**: Compile-time, type-safe, Google-backed, complex setup
|
|
313
|
-
- **Koin**: Runtime, simple DSL, faster setup, reflection-based
|
|
314
|
-
|
|
315
|
-
## Performance Optimization
|
|
316
|
-
|
|
317
|
-
### R8 Optimization
|
|
318
|
-
|
|
319
|
-
**Automatic optimizations:**
|
|
320
|
-
- Code shrinking (remove unused)
|
|
321
|
-
- Obfuscation (rename classes/methods)
|
|
322
|
-
- Optimization (method inlining)
|
|
323
|
-
|
|
324
|
-
```groovy
|
|
325
|
-
// build.gradle
|
|
326
|
-
android {
|
|
327
|
-
buildTypes {
|
|
328
|
-
release {
|
|
329
|
-
minifyEnabled true
|
|
330
|
-
shrinkResources true
|
|
331
|
-
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
**Impact:**
|
|
338
|
-
- Smaller app size
|
|
339
|
-
- Faster startup
|
|
340
|
-
- Harder to reverse engineer
|
|
341
|
-
|
|
342
|
-
### Baseline Profiles
|
|
343
|
-
|
|
344
|
-
**Performance boost:**
|
|
345
|
-
- Faster startup
|
|
346
|
-
- Reduced jank in critical paths
|
|
347
|
-
- AOT compilation of hot code
|
|
348
|
-
|
|
349
|
-
```gradle
|
|
350
|
-
// build.gradle
|
|
351
|
-
dependencies {
|
|
352
|
-
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
|
|
353
|
-
}
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### Compose Performance
|
|
357
|
-
|
|
358
|
-
**1. Stability annotations:**
|
|
359
|
-
```kotlin
|
|
360
|
-
// Mark stable classes
|
|
361
|
-
@Stable
|
|
362
|
-
data class User(val name: String, val age: Int)
|
|
363
|
-
|
|
364
|
-
// Immutable collections
|
|
365
|
-
@Immutable
|
|
366
|
-
data class UserList(val users: List<User>)
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
**2. Avoid recomposition:**
|
|
370
|
-
```kotlin
|
|
371
|
-
// ❌ Bad: Recomposes every render
|
|
372
|
-
@Composable
|
|
373
|
-
fun UserList(users: List<User>) {
|
|
374
|
-
LazyColumn {
|
|
375
|
-
items(users) { user ->
|
|
376
|
-
Text(user.name) // Recreated every time
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ✅ Good: Use keys
|
|
382
|
-
@Composable
|
|
383
|
-
fun UserList(users: List<User>) {
|
|
384
|
-
LazyColumn {
|
|
385
|
-
items(users, key = { it.id }) { user ->
|
|
386
|
-
Text(user.name)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
**3. Remember expensive computations:**
|
|
393
|
-
```kotlin
|
|
394
|
-
@Composable
|
|
395
|
-
fun ExpensiveList(items: List<Item>) {
|
|
396
|
-
val sortedItems = remember(items) {
|
|
397
|
-
items.sortedBy { it.priority }
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
LazyColumn {
|
|
401
|
-
items(sortedItems) { item ->
|
|
402
|
-
ItemCard(item)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
## Testing
|
|
409
|
-
|
|
410
|
-
### Unit Testing (JUnit + MockK)
|
|
411
|
-
|
|
412
|
-
```kotlin
|
|
413
|
-
class UserViewModelTest {
|
|
414
|
-
private lateinit var viewModel: UserViewModel
|
|
415
|
-
private val mockRepository = mockk<UserRepository>()
|
|
416
|
-
|
|
417
|
-
@Before
|
|
418
|
-
fun setup() {
|
|
419
|
-
viewModel = UserViewModel(mockRepository)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
@Test
|
|
423
|
-
fun `loadUsers should update state with users`() = runTest {
|
|
424
|
-
// Given
|
|
425
|
-
val users = listOf(User("1", "Test", "test@example.com"))
|
|
426
|
-
coEvery { mockRepository.getUsers() } returns flowOf(Result.success(users))
|
|
427
|
-
|
|
428
|
-
// When
|
|
429
|
-
viewModel.loadUsers()
|
|
430
|
-
|
|
431
|
-
// Then
|
|
432
|
-
val state = viewModel.uiState.value
|
|
433
|
-
assertEquals(users, state.users)
|
|
434
|
-
assertFalse(state.isLoading)
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
### Compose Testing
|
|
440
|
-
|
|
441
|
-
```kotlin
|
|
442
|
-
class UserListScreenTest {
|
|
443
|
-
@get:Rule
|
|
444
|
-
val composeTestRule = createComposeRule()
|
|
445
|
-
|
|
446
|
-
@Test
|
|
447
|
-
fun displayUsers() {
|
|
448
|
-
val users = listOf(User("1", "John", "john@example.com"))
|
|
449
|
-
|
|
450
|
-
composeTestRule.setContent {
|
|
451
|
-
UserListScreen(
|
|
452
|
-
users = users,
|
|
453
|
-
onUserClick = {}
|
|
454
|
-
)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
composeTestRule.onNodeWithText("John").assertIsDisplayed()
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### Instrumented Testing (Espresso)
|
|
463
|
-
|
|
464
|
-
```kotlin
|
|
465
|
-
@RunWith(AndroidJUnit4::class)
|
|
466
|
-
class LoginActivityTest {
|
|
467
|
-
@get:Rule
|
|
468
|
-
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
|
469
|
-
|
|
470
|
-
@Test
|
|
471
|
-
fun loginFlow() {
|
|
472
|
-
onView(withId(R.id.emailField))
|
|
473
|
-
.perform(typeText("test@example.com"))
|
|
474
|
-
|
|
475
|
-
onView(withId(R.id.passwordField))
|
|
476
|
-
.perform(typeText("password123"))
|
|
477
|
-
|
|
478
|
-
onView(withId(R.id.loginButton))
|
|
479
|
-
.perform(click())
|
|
480
|
-
|
|
481
|
-
onView(withText("Welcome"))
|
|
482
|
-
.check(matches(isDisplayed()))
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
## Material Design 3
|
|
488
|
-
|
|
489
|
-
### Theme Setup
|
|
490
|
-
|
|
491
|
-
```kotlin
|
|
492
|
-
@Composable
|
|
493
|
-
fun AppTheme(
|
|
494
|
-
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
495
|
-
dynamicColor: Boolean = true,
|
|
496
|
-
content: @Composable () -> Unit
|
|
497
|
-
) {
|
|
498
|
-
val colorScheme = when {
|
|
499
|
-
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
500
|
-
val context = LocalContext.current
|
|
501
|
-
if (darkTheme) dynamicDarkColorScheme(context)
|
|
502
|
-
else dynamicLightColorScheme(context)
|
|
503
|
-
}
|
|
504
|
-
darkTheme -> DarkColorScheme
|
|
505
|
-
else -> LightColorScheme
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
MaterialTheme(
|
|
509
|
-
colorScheme = colorScheme,
|
|
510
|
-
typography = Typography,
|
|
511
|
-
content = content
|
|
512
|
-
)
|
|
513
|
-
}
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
### Material Components
|
|
517
|
-
|
|
518
|
-
```kotlin
|
|
519
|
-
// Cards
|
|
520
|
-
Card(
|
|
521
|
-
modifier = Modifier.fillMaxWidth(),
|
|
522
|
-
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
523
|
-
) {
|
|
524
|
-
Text("Content")
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// FAB
|
|
528
|
-
FloatingActionButton(onClick = { /* Do something */ }) {
|
|
529
|
-
Icon(Icons.Default.Add, contentDescription = "Add")
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Navigation
|
|
533
|
-
NavigationBar {
|
|
534
|
-
items.forEach { item ->
|
|
535
|
-
NavigationBarItem(
|
|
536
|
-
icon = { Icon(item.icon, contentDescription = null) },
|
|
537
|
-
label = { Text(item.label) },
|
|
538
|
-
selected = selectedItem == item,
|
|
539
|
-
onClick = { selectedItem = item }
|
|
540
|
-
)
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
## Google Play Requirements (2024-2025)
|
|
546
|
-
|
|
547
|
-
### SDK Requirements
|
|
548
|
-
- **Current**: Target Android 14 (API 34)
|
|
549
|
-
- **Mandatory (Aug 31, 2025)**: Target Android 15 (API 35)
|
|
550
|
-
|
|
551
|
-
### Privacy & Security
|
|
552
|
-
- **Privacy policy**: Required for apps collecting data
|
|
553
|
-
- **Data safety**: Form in Play Console
|
|
554
|
-
- **Permissions**: Request only needed, justify dangerous permissions
|
|
555
|
-
- **Encryption**: HTTPS for network, KeyStore for sensitive data
|
|
556
|
-
|
|
557
|
-
### AAB (Android App Bundle)
|
|
558
|
-
```gradle
|
|
559
|
-
android {
|
|
560
|
-
bundle {
|
|
561
|
-
density {
|
|
562
|
-
enableSplit true
|
|
563
|
-
}
|
|
564
|
-
abi {
|
|
565
|
-
enableSplit true
|
|
566
|
-
}
|
|
567
|
-
language {
|
|
568
|
-
enableSplit true
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
**Benefits:**
|
|
575
|
-
- Smaller downloads
|
|
576
|
-
- Dynamic feature modules
|
|
577
|
-
- Instant apps support
|
|
578
|
-
|
|
579
|
-
## Common Pitfalls
|
|
580
|
-
|
|
581
|
-
1. **Main thread blocking**: Use coroutines with Dispatchers.IO
|
|
582
|
-
2. **Memory leaks**: Unregister listeners, cancel coroutines
|
|
583
|
-
3. **Configuration changes**: Use ViewModel, avoid Activity references
|
|
584
|
-
4. **Large images**: Use Coil/Glide for caching and resizing
|
|
585
|
-
5. **Forgetting permissions**: Runtime permission requests
|
|
586
|
-
6. **Ignoring Android versions**: Test on multiple API levels
|
|
587
|
-
7. **Not handling back press**: OnBackPressedDispatcher
|
|
588
|
-
8. **Hardcoded strings**: Use strings.xml for localization
|
|
589
|
-
9. **Not using Proguard/R8**: Enable in release builds
|
|
590
|
-
10. **Ignoring battery**: Use WorkManager for background tasks
|
|
591
|
-
|
|
592
|
-
## Resources
|
|
593
|
-
|
|
594
|
-
**Official:**
|
|
595
|
-
- Kotlin Docs: https://kotlinlang.org/docs/home.html
|
|
596
|
-
- Compose Docs: https://developer.android.com/jetpack/compose
|
|
597
|
-
- Material 3: https://m3.material.io/
|
|
598
|
-
- Android Guides: https://developer.android.com/guide
|
|
599
|
-
|
|
600
|
-
**Community:**
|
|
601
|
-
- Android Weekly: https://androidweekly.net/
|
|
602
|
-
- Kt.Academy: https://kt.academy/
|
|
603
|
-
- Coding in Flow: https://codinginflow.com/
|
|
604
|
-
- Philipp Lackner: https://pl-coding.com/
|