oh-my-agent-commandcode 0.0.0-stage → 20.38.3
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 +35 -2
- package/filter-test-output.sh +52 -0
- package/mods/oma.ts +347 -0
- package/package.json +30 -4
- package/skills/_shared/conditional/experiment-ledger.md +28 -0
- package/skills/_shared/conditional/exploration-loop.md +22 -0
- package/skills/_shared/conditional/quality-score.md +33 -0
- package/skills/_shared/core/anti-ai-prose.md +191 -0
- package/skills/_shared/core/api-contracts/README.md +28 -0
- package/skills/_shared/core/api-contracts/template.md +90 -0
- package/skills/_shared/core/clarification-protocol.md +23 -0
- package/skills/_shared/core/code-intelligence.md +46 -0
- package/skills/_shared/core/collaboration-policy.md +28 -0
- package/skills/_shared/core/common-checklist.md +28 -0
- package/skills/_shared/core/context-budget.md +22 -0
- package/skills/_shared/core/context-loading.md +34 -0
- package/skills/_shared/core/difficulty-guide.md +26 -0
- package/skills/_shared/core/execution-policy.md +31 -0
- package/skills/_shared/core/lessons-learned.md +33 -0
- package/skills/_shared/core/prompt-structure.md +18 -0
- package/skills/_shared/core/quality-principles.md +11 -0
- package/skills/_shared/core/session-metrics.md +35 -0
- package/skills/_shared/core/skill-routing.md +39 -0
- package/skills/_shared/core/test-approach.md +54 -0
- package/skills/_shared/core/vendor-detection.md +52 -0
- package/skills/_shared/runtime/memory-protocol.md +118 -0
- package/skills/_shared/runtime/result-contract.md +64 -0
- package/skills/_shared/ui-interaction-quality.md +133 -0
- package/skills/oma-academic-writing/SKILL.md +181 -0
- package/skills/oma-academic-writing/resources/abstract-guide.md +51 -0
- package/skills/oma-academic-writing/resources/academic-verb-tiers.md +42 -0
- package/skills/oma-academic-writing/resources/anti-ai-checklist.md +52 -0
- package/skills/oma-academic-writing/resources/hedging-guide.md +132 -0
- package/skills/oma-academic-writing/resources/sentence-structure-reference.md +151 -0
- package/skills/oma-architecture/SKILL.md +203 -0
- package/skills/oma-architecture/resources/api-evolution.md +41 -0
- package/skills/oma-architecture/resources/checklist.md +42 -0
- package/skills/oma-architecture/resources/execution-protocol.md +115 -0
- package/skills/oma-architecture/resources/methodology-selection.md +92 -0
- package/skills/oma-architecture/resources/migration-patterns.md +28 -0
- package/skills/oma-architecture/resources/output-templates.md +111 -0
- package/skills/oma-architecture/resources/stakeholder-synthesis.md +80 -0
- package/skills/oma-backend/SKILL.md +202 -0
- package/skills/oma-backend/resources/checklist.md +45 -0
- package/skills/oma-backend/resources/error-playbook.md +97 -0
- package/skills/oma-backend/resources/execution-protocol.md +39 -0
- package/skills/oma-backend/resources/orm-reference.md +61 -0
- package/skills/oma-backend/variants/node/api-template.ts +315 -0
- package/skills/oma-backend/variants/node/snippets.md +475 -0
- package/skills/oma-backend/variants/node/stack.yaml +21 -0
- package/skills/oma-backend/variants/node/tech-stack.md +33 -0
- package/skills/oma-backend/variants/python/api-template.py +378 -0
- package/skills/oma-backend/variants/python/snippets.md +199 -0
- package/skills/oma-backend/variants/python/stack.yaml +18 -0
- package/skills/oma-backend/variants/python/tech-stack.md +45 -0
- package/skills/oma-backend/variants/rust/api-template.rs +635 -0
- package/skills/oma-backend/variants/rust/snippets.md +471 -0
- package/skills/oma-backend/variants/rust/stack.yaml +20 -0
- package/skills/oma-backend/variants/rust/tech-stack.md +36 -0
- package/skills/oma-backend/variants/stack.schema.json +73 -0
- package/skills/oma-brainstorm/SKILL.md +166 -0
- package/skills/oma-brainstorm/resources/triz-lite.md +117 -0
- package/skills/oma-coordination/SKILL.md +171 -0
- package/skills/oma-db/SKILL.md +201 -0
- package/skills/oma-db/resources/anti-patterns.md +177 -0
- package/skills/oma-db/resources/checklist.md +67 -0
- package/skills/oma-db/resources/document-templates.md +107 -0
- package/skills/oma-db/resources/error-playbook.md +50 -0
- package/skills/oma-db/resources/examples.md +170 -0
- package/skills/oma-db/resources/execution-protocol.md +127 -0
- package/skills/oma-db/resources/iso-controls.md +98 -0
- package/skills/oma-db/resources/migration-playbook.md +93 -0
- package/skills/oma-db/resources/query-tuning.md +57 -0
- package/skills/oma-db/resources/vector-db.md +159 -0
- package/skills/oma-debug/SKILL.md +146 -0
- package/skills/oma-debug/resources/bug-report-template.md +327 -0
- package/skills/oma-debug/resources/checklist.md +31 -0
- package/skills/oma-debug/resources/common-patterns.md +734 -0
- package/skills/oma-debug/resources/debugging-checklist.md +364 -0
- package/skills/oma-debug/resources/error-playbook.md +94 -0
- package/skills/oma-debug/resources/execution-protocol.md +40 -0
- package/skills/oma-design/SKILL.md +135 -0
- package/skills/oma-design/resources/asset-strategy.md +34 -0
- package/skills/oma-design/resources/checklist.md +63 -0
- package/skills/oma-design/resources/data-grid.md +48 -0
- package/skills/oma-design/resources/design-md-spec.md +107 -0
- package/skills/oma-design/resources/design-tokens.md +53 -0
- package/skills/oma-design/resources/error-playbook.md +18 -0
- package/skills/oma-design/resources/execution-protocol.md +95 -0
- package/skills/oma-design/resources/external-design-skills.md +130 -0
- package/skills/oma-design/resources/getdesign-fetcher.md +104 -0
- package/skills/oma-design/resources/product-interface-review.md +59 -0
- package/skills/oma-design/resources/redesign-protocol.md +52 -0
- package/skills/oma-design/resources/stitch-integration.md +244 -0
- package/skills/oma-dev-workflow/SKILL.md +375 -0
- package/skills/oma-dev-workflow/resources/affected-checks.py +58 -0
- package/skills/oma-dev-workflow/resources/api-workflows.md +58 -0
- package/skills/oma-dev-workflow/resources/database-patterns.md +107 -0
- package/skills/oma-dev-workflow/resources/i18n-patterns.md +63 -0
- package/skills/oma-dev-workflow/resources/release-coordination.md +240 -0
- package/skills/oma-dev-workflow/resources/tooling-profiles.md +217 -0
- package/skills/oma-dev-workflow/resources/troubleshooting.md +78 -0
- package/skills/oma-dev-workflow/resources/validation-pipeline.md +246 -0
- package/skills/oma-docs/SKILL.md +77 -0
- package/skills/oma-docs/resources/commands.md +76 -0
- package/skills/oma-explanation/SKILL.md +212 -0
- package/skills/oma-explanation/resources/document-structure.md +115 -0
- package/skills/oma-explanation/resources/draft-format.md +199 -0
- package/skills/oma-explanation/resources/html-contract.md +102 -0
- package/skills/oma-frontend/SKILL.md +227 -0
- package/skills/oma-frontend/resources/angular-rules.md +110 -0
- package/skills/oma-frontend/resources/checklist.md +76 -0
- package/skills/oma-frontend/resources/error-playbook.md +125 -0
- package/skills/oma-frontend/resources/execution-protocol.md +46 -0
- package/skills/oma-frontend/resources/snippets.md +354 -0
- package/skills/oma-frontend/resources/specialized-visual-implementation.md +193 -0
- package/skills/oma-frontend/resources/tailwind-rules.md +83 -0
- package/skills/oma-frontend/resources/tech-stack.md +156 -0
- package/skills/oma-hwp/SKILL.md +166 -0
- package/skills/oma-hwp/config/hwp-config.yaml +35 -0
- package/skills/oma-hwp/resources/balanced-tables.ts +39 -0
- package/skills/oma-hwp/resources/execution-protocol.md +145 -0
- package/skills/oma-hwp/resources/flatten-tables.ts +101 -0
- package/skills/oma-hwp/resources/package.json +13 -0
- package/skills/oma-hwp/resources/troubleshooting.md +70 -0
- package/skills/oma-hwp/resources/tsconfig.json +10 -0
- package/skills/oma-hwp/resources/turndown-plugin-gfm.d.ts +9 -0
- package/skills/oma-image/SKILL.md +142 -0
- package/skills/oma-image/config/image-config.yaml +70 -0
- package/skills/oma-image/resources/checklist.md +18 -0
- package/skills/oma-image/resources/execution-protocol.md +117 -0
- package/skills/oma-image/resources/invocation.md +105 -0
- package/skills/oma-image/resources/prompt-tips.md +71 -0
- package/skills/oma-image/resources/vendor-matrix.md +90 -0
- package/skills/oma-market/SKILL.md +142 -0
- package/skills/oma-market/resources/checklist.md +12 -0
- package/skills/oma-market/resources/error-playbook.md +12 -0
- package/skills/oma-market/resources/execution-protocol.md +60 -0
- package/skills/oma-market/resources/frameworks/pestel.md +101 -0
- package/skills/oma-market/resources/frameworks/porters-5f.md +90 -0
- package/skills/oma-market/resources/frameworks/swot.md +61 -0
- package/skills/oma-market/resources/intent-rules.md +35 -0
- package/skills/oma-market/resources/output-laws.md +53 -0
- package/skills/oma-mobile/SKILL.md +168 -0
- package/skills/oma-mobile/resources/checklist.md +77 -0
- package/skills/oma-mobile/resources/error-playbook.md +198 -0
- package/skills/oma-mobile/resources/execution-protocol.md +47 -0
- package/skills/oma-mobile/resources/screen-template.dart +331 -0
- package/skills/oma-mobile/resources/screen-template.swift +239 -0
- package/skills/oma-mobile/resources/screen-template.tsx +179 -0
- package/skills/oma-mobile/resources/tech-stack.md +69 -0
- package/skills/oma-mobile/variants/README.md +45 -0
- package/skills/oma-mobile/variants/flutter/api-template.dart +461 -0
- package/skills/oma-mobile/variants/flutter/snippets.md +1353 -0
- package/skills/oma-mobile/variants/flutter/stack.yaml +21 -0
- package/skills/oma-mobile/variants/flutter/tech-stack.md +201 -0
- package/skills/oma-mobile/variants/kotlin-android/api-template.kt +182 -0
- package/skills/oma-mobile/variants/kotlin-android/snippets.md +871 -0
- package/skills/oma-mobile/variants/kotlin-android/stack.yaml +32 -0
- package/skills/oma-mobile/variants/kotlin-android/tech-stack.md +159 -0
- package/skills/oma-mobile/variants/react-native/api-template.ts +531 -0
- package/skills/oma-mobile/variants/react-native/snippets.md +1014 -0
- package/skills/oma-mobile/variants/react-native/stack.yaml +22 -0
- package/skills/oma-mobile/variants/react-native/tech-stack.md +192 -0
- package/skills/oma-mobile/variants/stack.schema.json +68 -0
- package/skills/oma-mobile/variants/swift-ios/api-template.swift +165 -0
- package/skills/oma-mobile/variants/swift-ios/snippets.md +1028 -0
- package/skills/oma-mobile/variants/swift-ios/stack.yaml +25 -0
- package/skills/oma-mobile/variants/swift-ios/tech-stack.md +174 -0
- package/skills/oma-observability/SKILL.md +225 -0
- package/skills/oma-observability/resources/anti-patterns.md +555 -0
- package/skills/oma-observability/resources/boundaries/cross-application.md +348 -0
- package/skills/oma-observability/resources/boundaries/multi-tenant.md +290 -0
- package/skills/oma-observability/resources/boundaries/release.md +372 -0
- package/skills/oma-observability/resources/boundaries/slo.md +225 -0
- package/skills/oma-observability/resources/checklist.md +123 -0
- package/skills/oma-observability/resources/examples.md +448 -0
- package/skills/oma-observability/resources/execution-protocol.md +166 -0
- package/skills/oma-observability/resources/incident-forensics.md +348 -0
- package/skills/oma-observability/resources/intent-rules.md +307 -0
- package/skills/oma-observability/resources/layers/L3-network.md +281 -0
- package/skills/oma-observability/resources/layers/L4-transport.md +299 -0
- package/skills/oma-observability/resources/layers/L7-application/crash-analytics.md +359 -0
- package/skills/oma-observability/resources/layers/L7-application/mobile-rum.md +278 -0
- package/skills/oma-observability/resources/layers/L7-application/waf.md +246 -0
- package/skills/oma-observability/resources/layers/L7-application/web-rum.md +350 -0
- package/skills/oma-observability/resources/layers/mesh.md +368 -0
- package/skills/oma-observability/resources/matrix.md +177 -0
- package/skills/oma-observability/resources/meta-observability.md +605 -0
- package/skills/oma-observability/resources/observability-as-code.md +666 -0
- package/skills/oma-observability/resources/signals/audit.md +310 -0
- package/skills/oma-observability/resources/signals/cost.md +303 -0
- package/skills/oma-observability/resources/signals/logs.md +263 -0
- package/skills/oma-observability/resources/signals/metrics.md +478 -0
- package/skills/oma-observability/resources/signals/privacy.md +351 -0
- package/skills/oma-observability/resources/signals/profiles.md +245 -0
- package/skills/oma-observability/resources/signals/traces.md +360 -0
- package/skills/oma-observability/resources/standards.md +214 -0
- package/skills/oma-observability/resources/transport/collector-topology.md +308 -0
- package/skills/oma-observability/resources/transport/otlp-grpc-vs-http.md +256 -0
- package/skills/oma-observability/resources/transport/sampling-recipes.md +297 -0
- package/skills/oma-observability/resources/transport/udp-statsd-mtu.md +167 -0
- package/skills/oma-observability/resources/vendor-categories.md +352 -0
- package/skills/oma-orchestration/SKILL.md +302 -0
- package/skills/oma-orchestration/config/cli-config.yaml +92 -0
- package/skills/oma-orchestration/resources/memory-schema.md +232 -0
- package/skills/oma-orchestration/resources/subagent-prompt-template.md +30 -0
- package/skills/oma-orchestration/scripts/parallel-run.sh +6 -0
- package/skills/oma-orchestration/scripts/spawn-agent.sh +5 -0
- package/skills/oma-orchestration/scripts/verify.sh +20 -0
- package/skills/oma-orchestration/templates/backend-task.md +19 -0
- package/skills/oma-orchestration/templates/debug-task.md +16 -0
- package/skills/oma-orchestration/templates/frontend-task.md +18 -0
- package/skills/oma-orchestration/templates/mobile-task.md +18 -0
- package/skills/oma-orchestration/templates/qa-task.md +16 -0
- package/skills/oma-orchestration/templates/tasks-example.yaml +15 -0
- package/skills/oma-pdf/SKILL.md +162 -0
- package/skills/oma-pdf/config/pdf-config.yaml +27 -0
- package/skills/oma-pdf/resources/execution-protocol.md +127 -0
- package/skills/oma-pm/SKILL.md +157 -0
- package/skills/oma-pm/resources/error-playbook.md +75 -0
- package/skills/oma-pm/resources/examples.md +439 -0
- package/skills/oma-pm/resources/execution-protocol.md +62 -0
- package/skills/oma-pm/resources/iso-planning.md +110 -0
- package/skills/oma-pm/resources/plan-phase-protocol.md +66 -0
- package/skills/oma-pm/resources/task-template.json +86 -0
- package/skills/oma-qa/SKILL.md +141 -0
- package/skills/oma-qa/resources/checklist.md +339 -0
- package/skills/oma-qa/resources/error-playbook.md +98 -0
- package/skills/oma-qa/resources/execution-protocol.md +169 -0
- package/skills/oma-qa/resources/iso-quality.md +82 -0
- package/skills/oma-qa/resources/verify-ship-protocol.md +116 -0
- package/skills/oma-recap/SKILL.md +245 -0
- package/skills/oma-recap/resources/output-formats.md +83 -0
- package/skills/oma-refactor/SKILL.md +184 -0
- package/skills/oma-refactor/resources/definition.md +108 -0
- package/skills/oma-refactor/resources/governance.md +33 -0
- package/skills/oma-refactor/resources/hotspots.py +44 -0
- package/skills/oma-refactor/resources/measurement.md +93 -0
- package/skills/oma-scholar/SKILL.md +276 -0
- package/skills/oma-scholar/config/scholar-config.yaml +79 -0
- package/skills/oma-scholar/resources/api-endpoints.md +174 -0
- package/skills/oma-scholar/resources/checklist.md +92 -0
- package/skills/oma-scholar/resources/execution-protocol.md +224 -0
- package/skills/oma-scholar/resources/fallback-providers.md +143 -0
- package/skills/oma-scholar/resources/setup-openalex.md +85 -0
- package/skills/oma-scholar/resources/sidecar-spec.md +341 -0
- package/skills/oma-scholar/resources/upstream-spec-cache.md +972 -0
- package/skills/oma-scm/SKILL.md +103 -0
- package/skills/oma-scm/resources/cm-operations.md +56 -0
- package/skills/oma-scm/resources/codeowners-playbook.md +45 -0
- package/skills/oma-scm/resources/conventional-commits.md +108 -0
- package/skills/oma-scm/resources/merge-risk.md +76 -0
- package/skills/oma-scm/resources/onboarding-risk-signals.md +71 -0
- package/skills/oma-search/SKILL.md +125 -0
- package/skills/oma-search/resources/checklist.md +36 -0
- package/skills/oma-search/resources/error-playbook.md +66 -0
- package/skills/oma-search/resources/examples.md +80 -0
- package/skills/oma-search/resources/execution-protocol.md +86 -0
- package/skills/oma-search/resources/intent-rules.md +47 -0
- package/skills/oma-search/resources/trust-registry.md +53 -0
- package/skills/oma-security/SKILL.md +172 -0
- package/skills/oma-security/resources/artex.md +108 -0
- package/skills/oma-security/resources/ci.md +134 -0
- package/skills/oma-security/resources/cisco-source.md +108 -0
- package/skills/oma-security/resources/deepsec-config.md +45 -0
- package/skills/oma-security/resources/deepsec-matchers.md +35 -0
- package/skills/oma-security/resources/deepsec-scanning.md +45 -0
- package/skills/oma-security/resources/deepsec-setup.md +41 -0
- package/skills/oma-security/resources/findings-contract.md +408 -0
- package/skills/oma-security/resources/mcp-scanning.md +122 -0
- package/skills/oma-security/resources/skill-scanning.md +106 -0
- package/skills/oma-security/resources/validation.md +71 -0
- package/skills/oma-skill-creation/SKILL.md +84 -0
- package/skills/oma-skill-creation/resources/prompt-evaluation.md +36 -0
- package/skills/oma-skill-creation/resources/ssl-lite-template.md +108 -0
- package/skills/oma-skill-creation/resources/validation-checklist.md +101 -0
- package/skills/oma-slide/SKILL.md +277 -0
- package/skills/oma-slide/resources/animation-patterns.md +245 -0
- package/skills/oma-slide/resources/assets/deck-stage.js +514 -0
- package/skills/oma-slide/resources/assets/viewport-base.css +257 -0
- package/skills/oma-slide/resources/canva-integration.md +203 -0
- package/skills/oma-slide/resources/checklist.md +46 -0
- package/skills/oma-slide/resources/design-doctrine.md +260 -0
- package/skills/oma-slide/resources/fixed-stage.md +232 -0
- package/skills/oma-slide/resources/generation-protocol.md +393 -0
- package/skills/oma-slide/resources/native-pptx.md +84 -0
- package/skills/oma-slide/resources/selection-index.json +461 -0
- package/skills/oma-slide/resources/style-presets.LICENSE +10 -0
- package/skills/oma-slide/resources/style-presets.md +520 -0
- package/skills/oma-tf-infra/SKILL.md +201 -0
- package/skills/oma-tf-infra/resources/checklist.md +53 -0
- package/skills/oma-tf-infra/resources/cost-optimization.md +574 -0
- package/skills/oma-tf-infra/resources/error-playbook.md +68 -0
- package/skills/oma-tf-infra/resources/examples.md +38 -0
- package/skills/oma-tf-infra/resources/execution-protocol.md +129 -0
- package/skills/oma-tf-infra/resources/iso-42001-infra.md +229 -0
- package/skills/oma-tf-infra/resources/multi-cloud-examples.md +241 -0
- package/skills/oma-tf-infra/resources/policy-testing-examples.md +332 -0
- package/skills/oma-translation/SKILL.md +229 -0
- package/skills/oma-translation/resources/anti-ai-patterns.md +104 -0
- package/skills/oma-translation/resources/lang/_template.md +73 -0
- package/skills/oma-translation/resources/lang/en.md +123 -0
- package/skills/oma-translation/resources/lang/ja.md +274 -0
- package/skills/oma-translation/resources/lang/ko.md +228 -0
- package/skills/oma-translation/resources/lang/zh.md +285 -0
- package/skills/oma-translation/resources/style-skills.md +104 -0
- package/skills/oma-translation/resources/translation-rubric.md +126 -0
- package/skills/oma-video/SKILL.md +229 -0
- package/skills/oma-video/config/video-config.yaml +60 -0
- package/skills/oma-video/resources/checklist.md +26 -0
- package/skills/oma-video/resources/execution-protocol.md +122 -0
- package/skills/oma-video/resources/existing-media.md +185 -0
- package/skills/oma-video/resources/hyperframes-authoring/README.md +44 -0
- package/skills/oma-video/resources/hyperframes-authoring/demo.md +31 -0
- package/skills/oma-video/resources/hyperframes-authoring/explainer.md +10 -0
- package/skills/oma-video/resources/hyperframes-authoring/shorts.md +10 -0
- package/skills/oma-video/resources/motion-principles.md +81 -0
- package/skills/oma-video/resources/mpt/driver.py +301 -0
- package/skills/oma-video/resources/prompt-tips.md +75 -0
- package/skills/oma-video/resources/script-schema.md +81 -0
- package/skills/oma-video/resources/sound-design.md +127 -0
- package/skills/oma-video/resources/strudel/package.json +11 -0
- package/skills/oma-video/resources/strudel/page.html +87 -0
- package/skills/oma-video/resources/strudel/render.mjs +295 -0
- package/skills/oma-video/resources/vendor-matrix.md +124 -0
- package/skills/oma-voice/SKILL.md +249 -0
- package/skills/oma-voice/config/voice-config.yaml +30 -0
- package/skills/oma-voice/resources/audio-processing.md +119 -0
- package/skills/oma-voice/resources/checklist.md +47 -0
- package/skills/oma-voice/resources/execution-protocol.md +215 -0
- package/skills/oma-voice/resources/prompt-tips.md +43 -0
- package/skills/oma-voice/resources/voice-matrix.md +51 -0
- package/skills/oma-voice/resources/voicestudio.md +87 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen Template for Mobile Agent (Flutter)
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the stack's canonical screen patterns:
|
|
5
|
+
* - A `@riverpod` codegen notifier (StreamNotifier) consuming a repository
|
|
6
|
+
* interface — never a hand-written global FutureProvider with an inline API call.
|
|
7
|
+
* - GoRouter typed-route navigation (`GoRouteData` subclass `.push(context)`),
|
|
8
|
+
* never Navigator 1.0 named routes (`pushNamed` / `static routeName`).
|
|
9
|
+
* - AsyncValue.when → loading / data / empty / error(retry) states.
|
|
10
|
+
* - Controller lifecycle (create in field/initState, dispose in dispose()).
|
|
11
|
+
* - Dark-mode-safe colors via `Theme.of(context).colorScheme` and
|
|
12
|
+
* `withValues(alpha:)` (NOT the deprecated `withOpacity`).
|
|
13
|
+
*
|
|
14
|
+
* This is a reference: the notifier, repository, and routes normally live in
|
|
15
|
+
* separate files (see variants/flutter/snippets.md §2/§3/§4/§6). Shown together
|
|
16
|
+
* here for readability; the `part` directive pulls in build_runner output.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import 'package:flutter/material.dart';
|
|
20
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
21
|
+
import 'package:go_router/go_router.dart';
|
|
22
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
23
|
+
|
|
24
|
+
part 'example_screen.g.dart';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// DOMAIN — entity + repository interface (normally under features/example/domain/)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/// Minimal domain entity. In a real feature use freezed (see snippets §3a).
|
|
31
|
+
class Example {
|
|
32
|
+
const Example({required this.id, required this.title, this.description});
|
|
33
|
+
final int id;
|
|
34
|
+
final String title;
|
|
35
|
+
final String? description;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The presentation layer depends on this interface, never on a concrete impl.
|
|
39
|
+
/// Swap the offline-first impl, a stub, or a mock behind this seam.
|
|
40
|
+
abstract interface class ExampleRepository {
|
|
41
|
+
/// Emits warm cached data immediately; cold loads await network and surface errors.
|
|
42
|
+
Stream<List<Example>> watchAll();
|
|
43
|
+
/// Force a network refresh, bypass TTL, and propagate failures.
|
|
44
|
+
Future<void> refresh();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// DI seam. Override this in `ProviderScope(overrides: [...])` (or a codegen
|
|
48
|
+
/// provider that returns the real `ExampleRepositoryImpl`) at app startup.
|
|
49
|
+
@riverpod
|
|
50
|
+
ExampleRepository exampleRepository(Ref ref) =>
|
|
51
|
+
throw UnimplementedError('Override exampleRepositoryProvider with a real impl.');
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// STATE — @riverpod StreamNotifier consuming the repository (features/example/presentation/)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/// Exposes the example list as `AsyncValue<List<Example>>`.
|
|
58
|
+
///
|
|
59
|
+
/// Riverpod converts the repository's `Stream` into an `AsyncValue`: loading on
|
|
60
|
+
/// first subscription, data(cachedList) on the first emission, then data(freshList)
|
|
61
|
+
/// after revalidation — no loading flicker when the cache is warm.
|
|
62
|
+
@riverpod
|
|
63
|
+
class ExamplesNotifier extends _$ExamplesNotifier {
|
|
64
|
+
@override
|
|
65
|
+
Stream<List<Example>> build() {
|
|
66
|
+
final repo = ref.watch(exampleRepositoryProvider);
|
|
67
|
+
return repo.watchAll();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// NAVIGATION — typed routes (features/example/... registered on the app router)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/// Typed detail route. `go_router_builder` generates the `.push()` / `.go()`
|
|
76
|
+
/// helpers from `@TypedGoRoute`; the annotation and `GoRouteData` come from
|
|
77
|
+
/// `package:go_router/go_router.dart`. Navigate with:
|
|
78
|
+
/// `const ExampleDetailRoute(id: 42).push(context);`
|
|
79
|
+
@TypedGoRoute<ExampleDetailRoute>(path: '/examples/:id')
|
|
80
|
+
class ExampleDetailRoute extends GoRouteData with $ExampleDetailRoute {
|
|
81
|
+
const ExampleDetailRoute({required this.id});
|
|
82
|
+
final int id;
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
Widget build(BuildContext context, GoRouterState state) =>
|
|
86
|
+
// Replace with the real detail screen.
|
|
87
|
+
Scaffold(appBar: AppBar(title: Text('Example $id')));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// SCREEN
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/// Example screen demonstrating common patterns.
|
|
95
|
+
class ExampleScreen extends ConsumerStatefulWidget {
|
|
96
|
+
const ExampleScreen({super.key});
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
ConsumerState<ExampleScreen> createState() => _ExampleScreenState();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class _ExampleScreenState extends ConsumerState<ExampleScreen> {
|
|
103
|
+
// Local UI state and controllers.
|
|
104
|
+
final _scrollController = ScrollController();
|
|
105
|
+
bool _showScrollToTop = false;
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
void initState() {
|
|
109
|
+
super.initState();
|
|
110
|
+
_setupScrollListener();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
void dispose() {
|
|
115
|
+
_scrollController.dispose(); // always dispose controllers
|
|
116
|
+
super.dispose();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
void _setupScrollListener() {
|
|
120
|
+
_scrollController.addListener(() {
|
|
121
|
+
final shouldShow = _scrollController.offset > 200;
|
|
122
|
+
if (shouldShow != _showScrollToTop) {
|
|
123
|
+
setState(() => _showScrollToTop = shouldShow);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@override
|
|
129
|
+
Widget build(BuildContext context) {
|
|
130
|
+
// Watch the notifier — rebuilds on every AsyncValue transition.
|
|
131
|
+
final examplesAsync = ref.watch(examplesNotifierProvider);
|
|
132
|
+
|
|
133
|
+
return Scaffold(
|
|
134
|
+
appBar: AppBar(
|
|
135
|
+
title: const Text('Example Screen'),
|
|
136
|
+
actions: [
|
|
137
|
+
IconButton(
|
|
138
|
+
icon: const Icon(Icons.refresh),
|
|
139
|
+
onPressed: () async {
|
|
140
|
+
try {
|
|
141
|
+
await ref.read(exampleRepositoryProvider).refresh();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (!context.mounted) return;
|
|
144
|
+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Refresh failed: $error')));
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
tooltip: 'Refresh',
|
|
148
|
+
),
|
|
149
|
+
IconButton(
|
|
150
|
+
icon: const Icon(Icons.settings),
|
|
151
|
+
// GoRouter imperative push; prefer a typed route when the
|
|
152
|
+
// destination is in the typed route graph.
|
|
153
|
+
onPressed: () => context.push('/settings'),
|
|
154
|
+
tooltip: 'Settings',
|
|
155
|
+
),
|
|
156
|
+
],
|
|
157
|
+
),
|
|
158
|
+
body: examplesAsync.when(
|
|
159
|
+
// Warm cache renders immediately; a background revalidation does not
|
|
160
|
+
// re-show the spinner (skipLoadingOnRefresh is on by default).
|
|
161
|
+
data: (items) =>
|
|
162
|
+
items.isEmpty ? _buildEmptyState() : _buildContent(items),
|
|
163
|
+
loading: () => const Center(child: CircularProgressIndicator()),
|
|
164
|
+
error: (error, _) => _buildErrorState(error),
|
|
165
|
+
),
|
|
166
|
+
floatingActionButton: _showScrollToTop
|
|
167
|
+
? FloatingActionButton(
|
|
168
|
+
onPressed: _scrollToTop,
|
|
169
|
+
tooltip: 'Scroll to top',
|
|
170
|
+
child: const Icon(Icons.arrow_upward),
|
|
171
|
+
)
|
|
172
|
+
: null,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Builds main content.
|
|
177
|
+
Widget _buildContent(List<Example> items) {
|
|
178
|
+
return RefreshIndicator(
|
|
179
|
+
onRefresh: () async {
|
|
180
|
+
try {
|
|
181
|
+
await ref.read(exampleRepositoryProvider).refresh();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (!context.mounted) return;
|
|
184
|
+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Refresh failed: $error')));
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
child: CustomScrollView(
|
|
188
|
+
controller: _scrollController,
|
|
189
|
+
slivers: [
|
|
190
|
+
SliverToBoxAdapter(
|
|
191
|
+
child: Padding(
|
|
192
|
+
padding: const EdgeInsets.all(16),
|
|
193
|
+
child: Text(
|
|
194
|
+
'${items.length} Items',
|
|
195
|
+
style: Theme.of(context).textTheme.titleMedium,
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
SliverList(
|
|
200
|
+
delegate: SliverChildBuilderDelegate(
|
|
201
|
+
(context, index) => _buildListItem(items[index], index),
|
|
202
|
+
childCount: items.length,
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
],
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Builds an individual list item.
|
|
211
|
+
Widget _buildListItem(Example item, int index) {
|
|
212
|
+
return Card(
|
|
213
|
+
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
214
|
+
child: ListTile(
|
|
215
|
+
leading: CircleAvatar(child: Text('${index + 1}')),
|
|
216
|
+
title: Text(item.title),
|
|
217
|
+
subtitle: item.description != null ? Text(item.description!) : null,
|
|
218
|
+
trailing: const Icon(Icons.chevron_right),
|
|
219
|
+
// Typed-route navigation — no Navigator.pushNamed.
|
|
220
|
+
onTap: () => ExampleDetailRoute(id: item.id).push(context),
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Builds the empty state.
|
|
226
|
+
Widget _buildEmptyState() {
|
|
227
|
+
final theme = Theme.of(context);
|
|
228
|
+
return Center(
|
|
229
|
+
child: Column(
|
|
230
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
231
|
+
children: [
|
|
232
|
+
Icon(Icons.inbox, size: 64, color: theme.colorScheme.secondary),
|
|
233
|
+
const SizedBox(height: 16),
|
|
234
|
+
Text('No items yet', style: theme.textTheme.titleLarge),
|
|
235
|
+
const SizedBox(height: 8),
|
|
236
|
+
Text(
|
|
237
|
+
'Add your first item to get started',
|
|
238
|
+
style: theme.textTheme.bodyMedium?.copyWith(
|
|
239
|
+
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
const SizedBox(height: 24),
|
|
243
|
+
FilledButton.icon(
|
|
244
|
+
onPressed: _showAddDialog,
|
|
245
|
+
icon: const Icon(Icons.add),
|
|
246
|
+
label: const Text('Add Item'),
|
|
247
|
+
),
|
|
248
|
+
],
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Builds the error state with a retry affordance.
|
|
254
|
+
Widget _buildErrorState(Object error) {
|
|
255
|
+
final theme = Theme.of(context);
|
|
256
|
+
return Center(
|
|
257
|
+
child: Padding(
|
|
258
|
+
padding: const EdgeInsets.all(24),
|
|
259
|
+
child: Column(
|
|
260
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
261
|
+
children: [
|
|
262
|
+
Icon(Icons.error_outline, size: 64, color: theme.colorScheme.error),
|
|
263
|
+
const SizedBox(height: 16),
|
|
264
|
+
Text(
|
|
265
|
+
'Oops! Something went wrong',
|
|
266
|
+
style: theme.textTheme.titleLarge,
|
|
267
|
+
textAlign: TextAlign.center,
|
|
268
|
+
),
|
|
269
|
+
const SizedBox(height: 8),
|
|
270
|
+
Text(
|
|
271
|
+
error.toString(),
|
|
272
|
+
style: theme.textTheme.bodyMedium?.copyWith(
|
|
273
|
+
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
|
274
|
+
),
|
|
275
|
+
textAlign: TextAlign.center,
|
|
276
|
+
),
|
|
277
|
+
const SizedBox(height: 24),
|
|
278
|
+
FilledButton.icon(
|
|
279
|
+
onPressed: () => ref.invalidate(examplesNotifierProvider),
|
|
280
|
+
icon: const Icon(Icons.refresh),
|
|
281
|
+
label: const Text('Try Again'),
|
|
282
|
+
),
|
|
283
|
+
],
|
|
284
|
+
),
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Event handlers
|
|
290
|
+
|
|
291
|
+
void _scrollToTop() {
|
|
292
|
+
_scrollController.animateTo(
|
|
293
|
+
0,
|
|
294
|
+
duration: const Duration(milliseconds: 500),
|
|
295
|
+
curve: Curves.easeOut,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
void _showAddDialog() {
|
|
300
|
+
// Controller is local to the dialog and disposed when it closes.
|
|
301
|
+
final controller = TextEditingController();
|
|
302
|
+
showDialog<void>(
|
|
303
|
+
context: context,
|
|
304
|
+
builder: (ctx) => AlertDialog(
|
|
305
|
+
title: const Text('Add Item'),
|
|
306
|
+
content: TextField(
|
|
307
|
+
controller: controller,
|
|
308
|
+
autofocus: true,
|
|
309
|
+
decoration: const InputDecoration(
|
|
310
|
+
labelText: 'Title',
|
|
311
|
+
hintText: 'Enter title',
|
|
312
|
+
),
|
|
313
|
+
onSubmitted: (_) => Navigator.of(ctx).pop(),
|
|
314
|
+
),
|
|
315
|
+
actions: [
|
|
316
|
+
TextButton(
|
|
317
|
+
onPressed: () => Navigator.of(ctx).pop(),
|
|
318
|
+
child: const Text('Cancel'),
|
|
319
|
+
),
|
|
320
|
+
FilledButton(
|
|
321
|
+
onPressed: () {
|
|
322
|
+
// In a real screen: await ref.read(examplesNotifierProvider.notifier).create(...)
|
|
323
|
+
Navigator.of(ctx).pop();
|
|
324
|
+
},
|
|
325
|
+
child: const Text('Add'),
|
|
326
|
+
),
|
|
327
|
+
],
|
|
328
|
+
),
|
|
329
|
+
).whenComplete(controller.dispose);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen Template for Mobile Agent (Swift iOS Native)
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates best practices for SwiftUI screens:
|
|
5
|
+
* - @Observable view model with explicit loading/error/empty/data states
|
|
6
|
+
* - .task modifier for async data loading (auto-cancelled on disappear)
|
|
7
|
+
* - Retry action wired to the view model
|
|
8
|
+
* - NavigationStack integration
|
|
9
|
+
* - iOS HIG-aligned layout
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import SwiftUI
|
|
13
|
+
import Observation
|
|
14
|
+
|
|
15
|
+
// MARK: - View Model
|
|
16
|
+
|
|
17
|
+
/// All possible display states for the example screen.
|
|
18
|
+
enum ExampleViewState {
|
|
19
|
+
case idle
|
|
20
|
+
case loading
|
|
21
|
+
case loaded([ExampleItem])
|
|
22
|
+
case empty
|
|
23
|
+
case error(String)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// A plain value type representing one row in the list.
|
|
27
|
+
struct ExampleItem: Identifiable {
|
|
28
|
+
let id: String
|
|
29
|
+
let title: String
|
|
30
|
+
let subtitle: String
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Protocol-backed so the view model is testable with a mock service.
|
|
34
|
+
/// A real conformer is a Repository over the generated `swift-openapi-generator`
|
|
35
|
+
/// `Client` fronted by a `ResponseCache` (read-through, stale-while-revalidate) —
|
|
36
|
+
/// see variants/swift-ios/snippets.md §10 and variants/swift-ios/api-template.swift.
|
|
37
|
+
/// Never hand-roll `URLRequest`/`JSONDecoder` for endpoints in the OpenAPI spec.
|
|
38
|
+
protocol ExampleServiceProtocol {
|
|
39
|
+
func fetchItems() async throws -> [ExampleItem]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@MainActor
|
|
43
|
+
@Observable
|
|
44
|
+
final class ExampleViewModel {
|
|
45
|
+
// MARK: - State observed by the View
|
|
46
|
+
|
|
47
|
+
/// Current display state. The View switches on this value.
|
|
48
|
+
var viewState: ExampleViewState = .idle
|
|
49
|
+
|
|
50
|
+
// MARK: - Private
|
|
51
|
+
|
|
52
|
+
private let service: ExampleServiceProtocol
|
|
53
|
+
/// Retained so a new load or view disappearance can cancel active work.
|
|
54
|
+
/// Tests await load() directly.
|
|
55
|
+
private(set) var loadTask: Task<Void, Never>?
|
|
56
|
+
|
|
57
|
+
init(service: ExampleServiceProtocol) {
|
|
58
|
+
self.service = service
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// MARK: - Intents (called by the View)
|
|
62
|
+
|
|
63
|
+
/// Starts (or restarts) data loading. Safe to call multiple times.
|
|
64
|
+
func load() async {
|
|
65
|
+
// Cancel any in-flight request before starting a fresh one.
|
|
66
|
+
loadTask?.cancel()
|
|
67
|
+
viewState = .loading
|
|
68
|
+
|
|
69
|
+
let task = Task { [weak self] in
|
|
70
|
+
guard let self else { return }
|
|
71
|
+
do {
|
|
72
|
+
let items = try await service.fetchItems()
|
|
73
|
+
guard !Task.isCancelled else { return }
|
|
74
|
+
viewState = items.isEmpty ? .empty : .loaded(items)
|
|
75
|
+
} catch is CancellationError {
|
|
76
|
+
// Ignore — another load is replacing this one.
|
|
77
|
+
} catch {
|
|
78
|
+
guard !Task.isCancelled else { return }
|
|
79
|
+
viewState = .error(error.localizedDescription)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
loadTask = task
|
|
83
|
+
await withTaskCancellationHandler {
|
|
84
|
+
await task.value
|
|
85
|
+
} onCancel: {
|
|
86
|
+
task.cancel()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Convenience retry; identical to load() but named for the error-state button.
|
|
91
|
+
func retry() async { await load() }
|
|
92
|
+
|
|
93
|
+
/// Cancels button/retry loads too; awaited .task forwards caller cancellation.
|
|
94
|
+
func cancelLoad() { loadTask?.cancel() }
|
|
95
|
+
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - View
|
|
99
|
+
|
|
100
|
+
struct ExampleScreen: View {
|
|
101
|
+
// The View owns the view model via @State so it is scoped to this screen.
|
|
102
|
+
@State private var viewModel: ExampleViewModel
|
|
103
|
+
|
|
104
|
+
init(service: ExampleServiceProtocol) {
|
|
105
|
+
// State owns the instance lifetime; Observation tracks accessed properties.
|
|
106
|
+
_viewModel = State(wrappedValue: ExampleViewModel(service: service))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var body: some View {
|
|
110
|
+
// The router owns the enclosing NavigationStack(path:); a feature view
|
|
111
|
+
// assumes it is already inside one and never wraps itself in a stack.
|
|
112
|
+
content
|
|
113
|
+
.navigationTitle("Example")
|
|
114
|
+
.toolbar {
|
|
115
|
+
// .topBarTrailing (iOS 17+) replaces .navigationBarTrailing, which
|
|
116
|
+
// is formally deprecated at iOS 27.
|
|
117
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
118
|
+
Button {
|
|
119
|
+
Task { await viewModel.load() }
|
|
120
|
+
} label: {
|
|
121
|
+
Label("Refresh", systemImage: "arrow.clockwise")
|
|
122
|
+
}
|
|
123
|
+
// Disable the button while a load is in progress.
|
|
124
|
+
.disabled({
|
|
125
|
+
if case .loading = viewModel.viewState { return true }
|
|
126
|
+
return false
|
|
127
|
+
}())
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// .task is preferred over .onAppear for async work:
|
|
131
|
+
// it creates a structured Task that is cancelled when the View disappears.
|
|
132
|
+
.task { await viewModel.load() }
|
|
133
|
+
.onDisappear { viewModel.cancelLoad() }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - Content switch
|
|
137
|
+
|
|
138
|
+
/// Switches over the view model's state and renders the appropriate sub-view.
|
|
139
|
+
@ViewBuilder
|
|
140
|
+
private var content: some View {
|
|
141
|
+
switch viewModel.viewState {
|
|
142
|
+
case .idle, .loading:
|
|
143
|
+
loadingView
|
|
144
|
+
|
|
145
|
+
case .loaded(let items):
|
|
146
|
+
listView(items)
|
|
147
|
+
|
|
148
|
+
case .empty:
|
|
149
|
+
emptyView
|
|
150
|
+
|
|
151
|
+
case .error(let message):
|
|
152
|
+
errorView(message: message)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// MARK: - State sub-views
|
|
157
|
+
|
|
158
|
+
/// Shown while the first load is in progress.
|
|
159
|
+
private var loadingView: some View {
|
|
160
|
+
ProgressView("Loading…")
|
|
161
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Shown when data is available.
|
|
165
|
+
private func listView(_ items: [ExampleItem]) -> some View {
|
|
166
|
+
List(items) { item in
|
|
167
|
+
// Value-based navigation: push a route value, not an inline destination.
|
|
168
|
+
// The route layer registers the destination for `ExampleItem.ID` via
|
|
169
|
+
// `swipeBackDestination(for:)` on the router's NavigationStack — see
|
|
170
|
+
// variants/swift-ios/snippets.md §9.
|
|
171
|
+
NavigationLink(value: item.id) {
|
|
172
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
173
|
+
Text(item.title)
|
|
174
|
+
.font(.headline)
|
|
175
|
+
Text(item.subtitle)
|
|
176
|
+
.font(.subheadline)
|
|
177
|
+
.foregroundStyle(.secondary)
|
|
178
|
+
}
|
|
179
|
+
.padding(.vertical, 4)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Pull-to-refresh triggers a fresh load.
|
|
183
|
+
.refreshable { await viewModel.load() }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Shown when the API returns an empty collection.
|
|
187
|
+
private var emptyView: some View {
|
|
188
|
+
ContentUnavailableView(
|
|
189
|
+
"Nothing Here",
|
|
190
|
+
systemImage: "tray",
|
|
191
|
+
description: Text("There are no items yet. Create one to get started.")
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Shown when the load fails. Includes a labelled retry button.
|
|
196
|
+
private func errorView(message: String) -> some View {
|
|
197
|
+
VStack(spacing: 20) {
|
|
198
|
+
Image(systemName: "exclamationmark.triangle.fill")
|
|
199
|
+
.font(.system(size: 48))
|
|
200
|
+
.foregroundStyle(.red)
|
|
201
|
+
|
|
202
|
+
Text("Something went wrong")
|
|
203
|
+
.font(.title2)
|
|
204
|
+
.bold()
|
|
205
|
+
|
|
206
|
+
Text(message)
|
|
207
|
+
.font(.body)
|
|
208
|
+
.foregroundStyle(.secondary)
|
|
209
|
+
.multilineTextAlignment(.center)
|
|
210
|
+
.padding(.horizontal, 32)
|
|
211
|
+
|
|
212
|
+
Button {
|
|
213
|
+
Task { await viewModel.retry() }
|
|
214
|
+
} label: {
|
|
215
|
+
Label("Try Again", systemImage: "arrow.clockwise")
|
|
216
|
+
.padding(.horizontal, 8)
|
|
217
|
+
}
|
|
218
|
+
.buttonStyle(.borderedProminent)
|
|
219
|
+
}
|
|
220
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
221
|
+
.padding()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// MARK: - Preview
|
|
226
|
+
|
|
227
|
+
#Preview {
|
|
228
|
+
// Provide a lightweight stub for canvas previews.
|
|
229
|
+
struct PreviewService: ExampleServiceProtocol {
|
|
230
|
+
func fetchItems() async throws -> [ExampleItem] {
|
|
231
|
+
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
232
|
+
return [
|
|
233
|
+
ExampleItem(id: "1", title: "First Item", subtitle: "Detail about the first item"),
|
|
234
|
+
ExampleItem(id: "2", title: "Second Item", subtitle: "Detail about the second item"),
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return ExampleScreen(service: PreviewService())
|
|
239
|
+
}
|