moai-adk 0.4.5__py3-none-any.whl → 0.20.1__py3-none-any.whl
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.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +1 -1
- moai_adk/__main__.py +74 -1
- moai_adk/cli/commands/__init__.py +1 -1
- moai_adk/cli/commands/analyze.py +119 -0
- moai_adk/cli/commands/backup.py +25 -1
- moai_adk/cli/commands/doctor.py +31 -5
- moai_adk/cli/commands/improve_user_experience.py +307 -0
- moai_adk/cli/commands/init.py +111 -10
- moai_adk/cli/commands/status.py +33 -3
- moai_adk/cli/commands/update.py +921 -130
- moai_adk/cli/commands/validate_links.py +120 -0
- moai_adk/cli/prompts/init_prompts.py +22 -87
- moai_adk/core/analysis/__init__.py +9 -0
- moai_adk/core/analysis/session_analyzer.py +388 -0
- moai_adk/core/analysis/tag_chain_analyzer.py +344 -0
- moai_adk/core/analysis/tag_chain_repair.py +879 -0
- moai_adk/core/config/__init__.py +19 -0
- moai_adk/core/config/migration.py +235 -0
- moai_adk/core/git/__init__.py +1 -1
- moai_adk/core/git/branch.py +1 -1
- moai_adk/core/git/commit.py +1 -1
- moai_adk/core/git/manager.py +1 -1
- moai_adk/core/issue_creator.py +313 -0
- moai_adk/core/mcp/setup.py +56 -0
- moai_adk/core/mcp/setup_old.py +296 -0
- moai_adk/core/project/backup_utils.py +1 -1
- moai_adk/core/project/checker.py +2 -2
- moai_adk/core/project/detector.py +211 -12
- moai_adk/core/project/initializer.py +85 -15
- moai_adk/core/project/phase_executor.py +76 -13
- moai_adk/core/project/validator.py +13 -13
- moai_adk/core/quality/__init__.py +1 -1
- moai_adk/core/quality/trust_checker.py +1 -1
- moai_adk/core/quality/validators/__init__.py +1 -1
- moai_adk/core/quality/validators/base_validator.py +1 -1
- moai_adk/core/tags/__init__.py +86 -0
- moai_adk/core/tags/auto_corrector.py +693 -0
- moai_adk/core/tags/ci_validator.py +463 -0
- moai_adk/core/tags/cli.py +283 -0
- moai_adk/core/tags/generator.py +109 -0
- moai_adk/core/tags/inserter.py +99 -0
- moai_adk/core/tags/mapper.py +126 -0
- moai_adk/core/tags/parser.py +76 -0
- moai_adk/core/tags/policy_validator.py +580 -0
- moai_adk/core/tags/pre_commit_validator.py +421 -0
- moai_adk/core/tags/reporter.py +956 -0
- moai_adk/core/tags/rollback_manager.py +525 -0
- moai_adk/core/tags/tags.py +149 -0
- moai_adk/core/tags/validator.py +897 -0
- moai_adk/core/template/__init__.py +1 -1
- moai_adk/core/template/backup.py +1 -1
- moai_adk/core/template/merger.py +50 -1
- moai_adk/core/template/processor.py +119 -13
- moai_adk/core/template_engine.py +268 -0
- moai_adk/templates/.claude/agents/alfred/backend-expert.md +348 -0
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +209 -944
- moai_adk/templates/.claude/agents/alfred/database-expert.md +352 -0
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +34 -5
- moai_adk/templates/.claude/agents/alfred/devops-expert.md +464 -0
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +38 -8
- moai_adk/templates/.claude/agents/alfred/format-expert.md +469 -0
- moai_adk/templates/.claude/agents/alfred/frontend-expert.md +357 -0
- moai_adk/templates/.claude/agents/alfred/git-manager.md +128 -9
- moai_adk/templates/.claude/agents/alfred/implementation-planner.md +104 -6
- moai_adk/templates/.claude/agents/alfred/project-manager.md +88 -16
- moai_adk/templates/.claude/agents/alfred/quality-gate.md +36 -9
- moai_adk/templates/.claude/agents/alfred/security-expert.md +270 -0
- moai_adk/templates/.claude/agents/alfred/skill-factory.md +865 -0
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +214 -43
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +111 -9
- moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +309 -160
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +36 -7
- moai_adk/templates/.claude/agents/alfred/ui-ux-expert.md +605 -0
- moai_adk/templates/.claude/commands/alfred/0-project.md +393 -966
- moai_adk/templates/.claude/commands/alfred/1-plan.md +651 -367
- moai_adk/templates/.claude/commands/alfred/2-run.md +388 -241
- moai_adk/templates/.claude/commands/alfred/3-sync.md +1921 -410
- moai_adk/templates/.claude/commands/alfred/9-feedback.md +153 -0
- moai_adk/templates/.claude/commands/alfred/release-new.md +3604 -0
- moai_adk/templates/.claude/hooks/alfred/core/project.py +484 -20
- moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
- moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +108 -0
- moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +14 -6
- moai_adk/templates/.claude/hooks/alfred/post_tool__enable_streaming_ui.py +50 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__tag_auto_corrector.py +407 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +99 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__realtime_tag_monitor.py +335 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__tag_policy_validator.py +325 -0
- moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__auto_cleanup.py +580 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +298 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +170 -0
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/checkpoint.py +3 -3
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/context.py +5 -5
- moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +749 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +230 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/__init__.py +21 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/daily_analysis.py +351 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +154 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +174 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +87 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/user.py +61 -0
- moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +111 -0
- moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
- moai_adk/templates/.claude/hooks/alfred/utils/hook_config.py +94 -0
- moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +161 -0
- moai_adk/templates/.claude/output-styles/alfred/alfred-moai-adk-beginner.md +267 -0
- moai_adk/templates/.claude/output-styles/alfred/keating-personal-tutor.md +440 -0
- moai_adk/templates/.claude/output-styles/alfred/r2d2-agentic-coding.md +583 -0
- moai_adk/templates/.claude/settings.json +96 -14
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/SKILL.md +70 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/examples.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/reference.md +242 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/SKILL.md +237 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/examples.md +871 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/reference.md +653 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/README.md +162 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/SKILL.md +227 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/examples.md +354 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/reference.md +158 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/SKILL.md +179 -79
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/examples.md +117 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/scripts/pre-review-check.sh +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/SKILL.md +132 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/reference.md +444 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/SKILL.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/SKILL.md +51 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/examples.md +355 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/reference.md +239 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/SKILL.md +323 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/examples.md +286 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/reference.md +126 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/SKILL.md +229 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/reference.md +150 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/SKILL.md +87 -73
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/README.md +42 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/SKILL.md +429 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/examples.md +520 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/SKILL.md +89 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/examples.md +122 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/reference.md +369 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/SKILL.md +508 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/examples.md +481 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/reference.md +100 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/SKILL.md +77 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/examples.md +265 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/reference.md +539 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/SKILL.md +320 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/reference.md +84 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/README.md +137 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/SKILL.md +219 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples/validate-spec.sh +161 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples.md +541 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/reference.md +622 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/SKILL.md +19 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/reference.md +211 -0
- moai_adk/templates/.claude/skills/moai-alfred-workflow/SKILL.md +288 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/SKILL.md +269 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/templates/agent-template.md +32 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/SKILL.md +298 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/templates/CLAUDE-template.md +26 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/SKILL.md +307 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/templates/command-template.md +21 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/SKILL.md +252 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/pre-bash-check.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/preserve-permissions.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/validate-bash-command.py +24 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/SKILL.md +199 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/templates/settings-mcp-template.json +39 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/SKILL.md +316 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/templates/session-summary-template.md +18 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/SKILL.md +263 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/templates/settings-complete-template.json +30 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/CHECKLIST.md +482 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/EXAMPLES.md +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/INTERACTIVE-DISCOVERY.md +524 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/METADATA.md +477 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PARALLEL-ANALYSIS-REPORT.md +429 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PYTHON-VERSION-MATRIX.md +391 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-FACTORY-WORKFLOW.md +431 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-UPDATE-ADVISOR.md +577 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL.md +273 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STEP-BY-STEP-GUIDE.md +466 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STRUCTURE.md +583 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/WEB-RESEARCH.md +526 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/reference.md +608 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/generate-structure.sh +328 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/validate-skill.sh +312 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/SKILL_TEMPLATE.md +245 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/examples-template.md +285 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/reference-template.md +278 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/scripts-template.sh +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/SKILL.md +291 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/templates/SKILL-template.md +15 -0
- moai_adk/templates/.claude/skills/moai-change-logger/SKILL.md +563 -0
- moai_adk/templates/.claude/skills/moai-design-systems/SKILL.md +802 -0
- moai_adk/templates/.claude/skills/moai-design-systems/examples.md +1238 -0
- moai_adk/templates/.claude/skills/moai-design-systems/reference.md +673 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +234 -43
- moai_adk/templates/.claude/skills/moai-domain-backend/examples.md +1633 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/reference.md +660 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/SKILL.md +97 -69
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/SKILL.md +97 -72
- moai_adk/templates/.claude/skills/moai-domain-data-science/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-domain-database/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-database/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-domain-devops/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +102 -73
- moai_adk/templates/.claude/skills/moai-domain-frontend/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-domain-ml/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/SKILL.md +97 -67
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-security/SKILL.md +97 -79
- moai_adk/templates/.claude/skills/moai-domain-security/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-security/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/SKILL.md +97 -71
- moai_adk/templates/.claude/skills/moai-domain-web-api/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/SKILL.md +265 -64
- moai_adk/templates/.claude/skills/moai-essentials-debug/examples.md +1064 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/reference.md +1047 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/SKILL.md +87 -78
- moai_adk/templates/.claude/skills/moai-essentials-perf/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/SKILL.md +87 -70
- moai_adk/templates/.claude/skills/moai-essentials-refactor/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/SKILL.md +87 -86
- moai_adk/templates/.claude/skills/moai-essentials-review/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/SKILL.md +80 -62
- moai_adk/templates/.claude/skills/moai-foundation-ears/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/SKILL.md +207 -50
- moai_adk/templates/.claude/skills/moai-foundation-git/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/reference.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/SKILL.md +90 -71
- moai_adk/templates/.claude/skills/moai-foundation-langs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/SKILL.md +78 -58
- moai_adk/templates/.claude/skills/moai-foundation-specs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/SKILL.md +78 -51
- moai_adk/templates/.claude/skills/moai-foundation-tags/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/.!11330!examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/SKILL.md +253 -32
- moai_adk/templates/.claude/skills/moai-foundation-trust/examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/reference.md +1099 -0
- moai_adk/templates/.claude/skills/moai-jit-docs-enhanced/SKILL.md +460 -0
- moai_adk/templates/.claude/skills/moai-lang-c/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-c/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-c/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/SKILL.md +98 -76
- moai_adk/templates/.claude/skills/moai-lang-cpp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/SKILL.md +2358 -70
- moai_adk/templates/.claude/skills/moai-lang-csharp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/SKILL.md +2962 -68
- moai_adk/templates/.claude/skills/moai-lang-dart/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +1898 -70
- moai_adk/templates/.claude/skills/moai-lang-go/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-go/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +1465 -68
- moai_adk/templates/.claude/skills/moai-lang-java/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-java/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/SKILL.md +2364 -66
- moai_adk/templates/.claude/skills/moai-lang-javascript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/reference.md +32 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/SKILL.md +1630 -69
- moai_adk/templates/.claude/skills/moai-lang-kotlin/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +89 -61
- moai_adk/templates/.claude/skills/moai-lang-php/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-php/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +735 -66
- moai_adk/templates/.claude/skills/moai-lang-python/examples.md +624 -0
- moai_adk/templates/.claude/skills/moai-lang-python/reference.md +316 -0
- moai_adk/templates/.claude/skills/moai-lang-r/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-lang-r/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-r/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/SKILL.md +98 -73
- moai_adk/templates/.claude/skills/moai-lang-ruby/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +1834 -70
- moai_adk/templates/.claude/skills/moai-lang-rust/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +99 -74
- moai_adk/templates/.claude/skills/moai-lang-scala/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-lang-shell/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-sql/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/SKILL.md +1959 -69
- moai_adk/templates/.claude/skills/moai-lang-swift/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-template/SKILL.md +348 -0
- moai_adk/templates/.claude/skills/moai-lang-template/VARIABLES.md +98 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +1230 -66
- moai_adk/templates/.claude/skills/moai-lang-typescript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/reference.md +34 -0
- moai_adk/templates/.claude/skills/moai-learning-optimizer/SKILL.md +575 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/README.md +50 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/SKILL.md +304 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/examples.md +417 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/reference.md +704 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/README.md +87 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/examples.md +1109 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/reference.md +514 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/validate.py +106 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/README.md +11 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/SKILL.md +622 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/examples.md +20 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/reference.md +12 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/README.md +152 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/SKILL.md +285 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/examples.md +333 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/reference.md +386 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/README.md +49 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/SKILL.md +319 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/examples.md +58 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/reference.md +123 -0
- moai_adk/templates/.claude/skills/moai-session-info/SKILL.md +314 -0
- moai_adk/templates/.claude/skills/moai-streaming-ui/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-tag-policy-validator/SKILL.md +570 -0
- moai_adk/templates/.git-hooks/pre-commit +66 -0
- moai_adk/templates/.git-hooks/pre-push +255 -0
- moai_adk/templates/.github/workflows/c-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/cpp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/csharp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/dart-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/go-tag-validation.yml +130 -0
- moai_adk/templates/.github/workflows/java-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/javascript-tag-validation.yml +135 -0
- moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/moai-gitflow.yml +166 -3
- moai_adk/templates/.github/workflows/moai-release-create.yml +100 -0
- moai_adk/templates/.github/workflows/moai-release-pipeline.yml +188 -0
- moai_adk/templates/.github/workflows/php-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/python-tag-validation.yml +118 -0
- moai_adk/templates/.github/workflows/release.yml +118 -0
- moai_adk/templates/.github/workflows/ruby-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/rust-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/shell-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/spec-issue-sync.yml +338 -0
- moai_adk/templates/.github/workflows/swift-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/tag-report.yml +269 -0
- moai_adk/templates/.github/workflows/tag-validation.yml +186 -0
- moai_adk/templates/.github/workflows/typescript-tag-validation.yml +154 -0
- moai_adk/templates/.mcp.json +31 -0
- moai_adk/templates/.moai/config.json +80 -7
- moai_adk/templates/CLAUDE.md +562 -546
- moai_adk/utils/banner.py +5 -5
- moai_adk/utils/common.py +294 -0
- moai_adk/utils/link_validator.py +235 -0
- moai_adk/utils/logger.py +8 -8
- moai_adk/utils/user_experience.py +451 -0
- moai_adk-0.20.1.dist-info/METADATA +233 -0
- moai_adk-0.20.1.dist-info/RECORD +404 -0
- moai_adk/templates/.claude/hooks/alfred/README.md +0 -230
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +0 -156
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +0 -85
- moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +0 -25
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +0 -92
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +0 -70
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +0 -41
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +0 -636
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +0 -692
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +0 -470
- moai_adk/templates/.claude/skills/moai-alfred-debugger-pro/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-ears-authoring/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-git-workflow/SKILL.md +0 -95
- moai_adk/templates/.claude/skills/moai-alfred-performance-optimizer/SKILL.md +0 -105
- moai_adk/templates/.claude/skills/moai-alfred-refactoring-coach/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-spec-metadata-validation/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-tag-scanning/SKILL.md +0 -90
- moai_adk/templates/.claude/skills/moai-alfred-trust-validation/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/SKILL.md +0 -87
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/examples.md +0 -62
- moai_adk/templates/.claude/skills/moai-claude-code/SKILL.md +0 -94
- moai_adk/templates/.claude/skills/moai-claude-code/examples.md +0 -513
- moai_adk/templates/.claude/skills/moai-claude-code/reference.md +0 -433
- moai_adk/templates/.claude/skills/moai-claude-code/templates/agent-full.md +0 -332
- moai_adk/templates/.claude/skills/moai-claude-code/templates/command-full.md +0 -384
- moai_adk/templates/.claude/skills/moai-claude-code/templates/plugin-full.json +0 -363
- moai_adk/templates/.claude/skills/moai-claude-code/templates/settings-full.json +0 -595
- moai_adk/templates/.claude/skills/moai-claude-code/templates/skill-full.md +0 -496
- moai_adk/templates/.claude/skills/moai-lang-clojure/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-lang-haskell/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-julia/SKILL.md +0 -98
- moai_adk/templates/.claude/skills/moai-lang-lua/SKILL.md +0 -98
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +0 -69
- moai_adk/templates/.moai/memory/development-guide.md +0 -344
- moai_adk/templates/.moai/memory/gitflow-protection-policy.md +0 -220
- moai_adk/templates/.moai/memory/spec-metadata.md +0 -356
- moai_adk/templates/.moai/project/product.md +0 -161
- moai_adk/templates/.moai/project/structure.md +0 -156
- moai_adk/templates/.moai/project/tech.md +0 -227
- moai_adk/templates/__init__.py +0 -2
- moai_adk-0.4.5.dist-info/METADATA +0 -369
- moai_adk-0.4.5.dist-info/RECORD +0 -152
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/WHEEL +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,98 +1,2992 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
2
|
name: moai-lang-dart
|
|
4
|
-
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
created: 2025-11-06
|
|
5
|
+
updated: 2025-11-06
|
|
6
|
+
status: active
|
|
7
|
+
description: "Dart best practices with Flutter mobile development, async programming, and server-side Dart for 2025"
|
|
8
|
+
keywords: [dart, programming, flutter, mobile, async, server-side, development]
|
|
5
9
|
allowed-tools:
|
|
6
10
|
- Read
|
|
11
|
+
- Write
|
|
12
|
+
- Edit
|
|
7
13
|
- Bash
|
|
14
|
+
- WebFetch
|
|
15
|
+
- WebSearch
|
|
8
16
|
---
|
|
9
17
|
|
|
10
|
-
# Dart
|
|
18
|
+
# Dart Development Mastery
|
|
19
|
+
|
|
20
|
+
**Modern Dart Development with 2025 Best Practices**
|
|
21
|
+
|
|
22
|
+
> Comprehensive Dart development guidance covering Flutter mobile applications, async programming patterns, server-side development, and cross-platform solutions using the latest tools and frameworks.
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
11
25
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
| Trigger cues | Dart code discussions, framework guidance, or file extensions such as .dart. |
|
|
18
|
-
| Tier | 3 |
|
|
26
|
+
### Flutter Mobile Development
|
|
27
|
+
- **Mobile App Development**: Flutter 3.x with Material Design 3 and Cupertino widgets
|
|
28
|
+
- **State Management**: Provider, Riverpod, BLoC patterns for scalable applications
|
|
29
|
+
- **Navigation**: Go Router for declarative routing and deep linking
|
|
30
|
+
- **Performance**: Widget optimization, lazy loading, memory management
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
### Server-Side Development
|
|
33
|
+
- **Web APIs**: Shelf, Dart Frog, or Aqueduct for backend services
|
|
34
|
+
- **Database Integration**: PostgreSQL, MongoDB with async drivers
|
|
35
|
+
- **Real-time Communication**: WebSockets, gRPC with Dart's async capabilities
|
|
36
|
+
- **Testing**: Unit tests, widget tests, integration tests with Dart test framework
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
### Cross-Platform Development
|
|
39
|
+
- **Flutter for Multiple Platforms**: iOS, Android, Web, Desktop, and Embedded
|
|
40
|
+
- **Shared Codebases**: Dart packages for business logic across platforms
|
|
41
|
+
- **Platform-Specific APIs**: Method channels for native integration
|
|
23
42
|
|
|
24
|
-
## When to
|
|
43
|
+
## When to Use
|
|
25
44
|
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
45
|
+
### Perfect Scenarios
|
|
46
|
+
- **Building cross-platform mobile applications with Flutter**
|
|
47
|
+
- **Developing scalable Flutter apps with modern state management**
|
|
48
|
+
- **Creating server-side APIs with Dart**
|
|
49
|
+
- **Implementing real-time applications with WebSockets**
|
|
50
|
+
- **Building web applications with Flutter Web**
|
|
51
|
+
- **Developing desktop applications with Flutter Desktop**
|
|
52
|
+
- **Creating embedded systems applications**
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
### Common Triggers
|
|
55
|
+
- "Create Flutter app"
|
|
56
|
+
- "Build Dart web API"
|
|
57
|
+
- "Implement Flutter state management"
|
|
58
|
+
- "Optimize Flutter performance"
|
|
59
|
+
- "Test Flutter application"
|
|
60
|
+
- "Dart best practices"
|
|
32
61
|
|
|
33
|
-
|
|
34
|
-
- **flutter test**: Built-in test framework
|
|
35
|
-
- **mockito**: Mocking library for Dart
|
|
36
|
-
- **Widget testing**: Test Flutter widgets
|
|
37
|
-
- Test coverage with `flutter test --coverage`
|
|
62
|
+
## Tool Version Matrix (2025-11-06)
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
64
|
+
### Core Dart/Flutter
|
|
65
|
+
- **Dart**: 3.5.x (current) / 3.4.x (LTS)
|
|
66
|
+
- **Flutter**: 3.24.x (current) / 3.22.x (LTS)
|
|
67
|
+
- **Dart SDK**: 3.5.0
|
|
68
|
+
- **Package Manager**: pub (built-in)
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
47
|
-
-
|
|
70
|
+
### Flutter Frameworks
|
|
71
|
+
- **Material Design**: 3.x (Material 3)
|
|
72
|
+
- **Cupertino Widgets**: iOS 17+ support
|
|
73
|
+
- **Go Router**: 13.x - Declarative routing
|
|
74
|
+
- **Riverpod**: 2.5.x - Reactive state management
|
|
75
|
+
- **BLoC**: 8.1.x - State management library
|
|
48
76
|
|
|
49
|
-
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
77
|
+
### Testing Tools
|
|
78
|
+
- **Dart Test**: Built-in testing framework
|
|
79
|
+
- **Flutter Test**: Widget and integration testing
|
|
80
|
+
- **Mockito**: 5.4.x - Mocking framework
|
|
81
|
+
- **Golden Tests**: Widget screenshot testing
|
|
82
|
+
- **Integration Test**: End-to-end testing
|
|
54
83
|
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
84
|
+
### Development Tools
|
|
85
|
+
- **Flutter CLI**: 3.24.x
|
|
86
|
+
- **Dart DevTools**: Web-based debugging tools
|
|
87
|
+
- **Android Studio**: Dolphin 2024.x
|
|
88
|
+
- **VS Code**: Flutter extension
|
|
89
|
+
|
|
90
|
+
### Backend Tools
|
|
91
|
+
- **Shelf**: 1.4.x - Web server framework
|
|
92
|
+
- **Dart Frog**: 1.0.x - Server-side framework
|
|
93
|
+
- **Aqueduct**: 7.x - Full-stack framework
|
|
94
|
+
- **MongoDB Dart Driver**: 4.12.x
|
|
95
|
+
|
|
96
|
+
## Ecosystem Overview
|
|
97
|
+
|
|
98
|
+
### Package Management
|
|
60
99
|
|
|
61
|
-
## Examples
|
|
62
100
|
```bash
|
|
63
|
-
|
|
101
|
+
# Create new Flutter project
|
|
102
|
+
flutter create my_app
|
|
103
|
+
flutter create --org com.example --platforms=web,desktop my_app
|
|
104
|
+
|
|
105
|
+
# Create new Dart project
|
|
106
|
+
dart create my_dart_app
|
|
107
|
+
|
|
108
|
+
# Add dependencies
|
|
109
|
+
flutter pub add provider riverpod go_router
|
|
110
|
+
flutter pub add dio retrofit json_annotation
|
|
111
|
+
dart pub add shelf shelf_router
|
|
112
|
+
|
|
113
|
+
# Get dependencies
|
|
114
|
+
flutter pub get
|
|
115
|
+
dart pub get
|
|
116
|
+
|
|
117
|
+
# Run and build
|
|
118
|
+
flutter run
|
|
119
|
+
flutter run --release
|
|
120
|
+
flutter build apk
|
|
121
|
+
flutter build web
|
|
122
|
+
dart run
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Project Structure (2025 Best Practice)
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
my_flutter_app/
|
|
129
|
+
├── lib/
|
|
130
|
+
│ ├── main.dart # App entry point
|
|
131
|
+
│ ├── app.dart # App configuration
|
|
132
|
+
│ ├── core/ # Core utilities
|
|
133
|
+
│ │ ├── constants/ # App constants
|
|
134
|
+
│ │ ├── errors/ # Custom error classes
|
|
135
|
+
│ │ ├── extensions/ # Dart extensions
|
|
136
|
+
│ │ ├── network/ # Network configuration
|
|
137
|
+
│ │ ├── themes/ # App themes
|
|
138
|
+
│ │ └── utils/ # Utility functions
|
|
139
|
+
│ ├── features/ # Feature modules
|
|
140
|
+
│ │ ├── authentication/ # Auth feature
|
|
141
|
+
│ │ │ ├── data/ # Data layer (repositories, data sources)
|
|
142
|
+
│ │ │ ├── domain/ # Domain layer (entities, use cases)
|
|
143
|
+
│ │ │ └── presentation/ # UI layer (pages, widgets, providers)
|
|
144
|
+
│ │ ├── user_profile/ # User profile feature
|
|
145
|
+
│ │ └── settings/ # Settings feature
|
|
146
|
+
│ ├── shared/ # Shared components
|
|
147
|
+
│ │ ├── widgets/ # Reusable widgets
|
|
148
|
+
│ │ ├── models/ # Shared data models
|
|
149
|
+
│ │ ├── services/ # Shared services
|
|
150
|
+
│ │ └── providers/ # Shared providers
|
|
151
|
+
│ └── routes/ # App routes
|
|
152
|
+
├── test/ # Test files
|
|
153
|
+
│ ├── unit/ # Unit tests
|
|
154
|
+
│ ├── widget/ # Widget tests
|
|
155
|
+
│ └── integration/ # Integration tests
|
|
156
|
+
├── assets/ # Static assets
|
|
157
|
+
├── pubspec.yaml # Dependencies
|
|
158
|
+
└── analysis_options.yaml # Linting rules
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Modern Development Patterns
|
|
162
|
+
|
|
163
|
+
### Dart 3.x Language Features
|
|
164
|
+
|
|
165
|
+
```dart
|
|
166
|
+
// Enhanced patterns with records and pattern matching
|
|
167
|
+
sealed class NetworkResult<T> {
|
|
168
|
+
const NetworkResult();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class Success<T> extends NetworkResult<T> {
|
|
172
|
+
final T data;
|
|
173
|
+
const Success(this.data);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class Error<T> extends NetworkResult<T> {
|
|
177
|
+
final String message;
|
|
178
|
+
final Exception? exception;
|
|
179
|
+
const Error(this.message, [this.exception]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class Loading<T> extends NetworkResult<T> {
|
|
183
|
+
const Loading();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Pattern matching with switch expressions
|
|
187
|
+
T handleNetworkResult<T>(NetworkResult<T> result) {
|
|
188
|
+
return switch (result) {
|
|
189
|
+
Success(data: final data) => data,
|
|
190
|
+
Error(message: final message) => throw Exception(message),
|
|
191
|
+
Loading() => throw StateError('Still loading'),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Records for lightweight data structures
|
|
196
|
+
typedef UserInfo = (String name, int age, String email);
|
|
197
|
+
|
|
198
|
+
class UserService {
|
|
199
|
+
UserInfo getUserInfo(int id) {
|
|
200
|
+
return ('John Doe', 30, 'john@example.com');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
void printUserInfo(UserInfo user) {
|
|
204
|
+
final (name, age, email) = user;
|
|
205
|
+
print('$name, $age years old, email: $email');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Enhanced type inference and const constructors
|
|
210
|
+
class AppConfig {
|
|
211
|
+
static const apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'https://api.example.com');
|
|
212
|
+
static const appVersion = String.fromEnvironment('APP_VERSION', defaultValue: '1.0.0');
|
|
213
|
+
static const isDebug = bool.fromEnvironment('DEBUG', defaultValue: false);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Enhanced enums with methods and properties
|
|
217
|
+
enum ThemeMode {
|
|
218
|
+
light._('Light Theme', '☀️'),
|
|
219
|
+
dark._('Dark Theme', '🌙'),
|
|
220
|
+
system._('System Theme', '💻');
|
|
221
|
+
|
|
222
|
+
const ThemeMode._(this.displayName, this.icon);
|
|
223
|
+
|
|
224
|
+
final String displayName;
|
|
225
|
+
final String icon;
|
|
226
|
+
|
|
227
|
+
Brightness get brightness => switch (this) {
|
|
228
|
+
ThemeMode.light => Brightness.light,
|
|
229
|
+
ThemeMode.dark => Brightness.dark,
|
|
230
|
+
ThemeMode.system => PlatformDispatcher.instance.platformBrightness,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Extension methods for enhanced APIs
|
|
235
|
+
extension StringExtension on String {
|
|
236
|
+
bool get isValidEmail {
|
|
237
|
+
return RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(this);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
String get capitalize {
|
|
241
|
+
return '${this[0].toUpperCase()}${substring(1)}';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
String truncate(int length, {String suffix = '...'}) {
|
|
245
|
+
if (this.length <= length) return this;
|
|
246
|
+
return '${substring(0, length)}$suffix';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Modern Flutter State Management with Riverpod
|
|
252
|
+
|
|
253
|
+
```dart
|
|
254
|
+
// Provider setup with Riverpod 2.x
|
|
255
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
256
|
+
|
|
257
|
+
// Model classes with immutability
|
|
258
|
+
@immutable
|
|
259
|
+
class User {
|
|
260
|
+
final String id;
|
|
261
|
+
final String name;
|
|
262
|
+
final String email;
|
|
263
|
+
final String avatarUrl;
|
|
264
|
+
|
|
265
|
+
const User({
|
|
266
|
+
required this.id,
|
|
267
|
+
required this.name,
|
|
268
|
+
required this.email,
|
|
269
|
+
required this.avatarUrl,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
User copyWith({
|
|
273
|
+
String? id,
|
|
274
|
+
String? name,
|
|
275
|
+
String? email,
|
|
276
|
+
String? avatarUrl,
|
|
277
|
+
}) {
|
|
278
|
+
return User(
|
|
279
|
+
id: id ?? this.id,
|
|
280
|
+
name: name ?? this.name,
|
|
281
|
+
email: email ?? this.email,
|
|
282
|
+
avatarUrl: avatarUrl ?? this.avatarUrl,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@override
|
|
287
|
+
bool operator ==(Object other) {
|
|
288
|
+
if (identical(this, other)) return true;
|
|
289
|
+
return other is User &&
|
|
290
|
+
other.id == id &&
|
|
291
|
+
other.name == name &&
|
|
292
|
+
other.email == email;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@override
|
|
296
|
+
int get hashCode => id.hashCode ^ name.hashCode ^ email.hashCode;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Repository interface
|
|
300
|
+
abstract class UserRepository {
|
|
301
|
+
Future<User> getUser(String userId);
|
|
302
|
+
Future<List<User>> getUsers({int page = 1, int limit = 20});
|
|
303
|
+
Future<User> updateUser(User user);
|
|
304
|
+
Future<void> deleteUser(String userId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Implementation with HTTP client
|
|
308
|
+
class HttpUserRepository implements UserRepository {
|
|
309
|
+
final Dio _dio;
|
|
310
|
+
|
|
311
|
+
HttpUserRepository(this._dio);
|
|
312
|
+
|
|
313
|
+
@override
|
|
314
|
+
Future<User> getUser(String userId) async {
|
|
315
|
+
try {
|
|
316
|
+
final response = await _dio.get('/users/$userId');
|
|
317
|
+
return User.fromJson(response.data);
|
|
318
|
+
} on DioException catch (e) {
|
|
319
|
+
throw UserRepositoryException('Failed to get user: $e');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@override
|
|
324
|
+
Future<List<User>> getUsers({int page = 1, int limit = 20}) async {
|
|
325
|
+
try {
|
|
326
|
+
final response = await _dio.get('/users', queryParameters: {
|
|
327
|
+
'page': page,
|
|
328
|
+
'limit': limit,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return (response.data as List)
|
|
332
|
+
.map((json) => User.fromJson(json))
|
|
333
|
+
.toList();
|
|
334
|
+
} on DioException catch (e) {
|
|
335
|
+
throw UserRepositoryException('Failed to get users: $e');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@override
|
|
340
|
+
Future<User> updateUser(User user) async {
|
|
341
|
+
try {
|
|
342
|
+
final response = await _dio.put('/users/${user.id}', data: user.toJson());
|
|
343
|
+
return User.fromJson(response.data);
|
|
344
|
+
} on DioException catch (e) {
|
|
345
|
+
throw UserRepositoryException('Failed to update user: $e');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@override
|
|
350
|
+
Future<void> deleteUser(String userId) async {
|
|
351
|
+
try {
|
|
352
|
+
await _dio.delete('/users/$userId');
|
|
353
|
+
} on DioException catch (e) {
|
|
354
|
+
throw UserRepositoryException('Failed to delete user: $e');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Riverpod providers
|
|
360
|
+
final dioProvider = Provider<Dio>((ref) {
|
|
361
|
+
final dio = Dio(BaseOptions(baseUrl: AppConfig.apiBaseUrl));
|
|
362
|
+
dio.interceptors.add(LogInterceptor());
|
|
363
|
+
return dio;
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
final userRepositoryProvider = Provider<UserRepository>((ref) {
|
|
367
|
+
return HttpUserRepository(ref.read(dioProvider));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Async providers for data fetching
|
|
371
|
+
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
|
|
372
|
+
final repository = ref.watch(userRepositoryProvider);
|
|
373
|
+
return repository.getUser(userId);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
final usersProvider = AsyncNotifierProvider<UsersNotifier, List<User>>(UsersNotifier.new);
|
|
377
|
+
|
|
378
|
+
// Notifier for managing state
|
|
379
|
+
class UsersNotifier extends AsyncNotifier<List<User>> {
|
|
380
|
+
int _page = 1;
|
|
381
|
+
final int _limit = 20;
|
|
382
|
+
bool _hasMore = true;
|
|
383
|
+
|
|
384
|
+
@override
|
|
385
|
+
Future<List<User>> build() async {
|
|
386
|
+
return _loadUsers();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
Future<List<User>> _loadUsers() async {
|
|
390
|
+
if (state.isLoading || !_hasMore) return state.value ?? [];
|
|
391
|
+
|
|
392
|
+
state = const AsyncLoading();
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
final repository = ref.read(userRepositoryProvider);
|
|
396
|
+
final newUsers = await repository.getUsers(page: _page, limit: _limit);
|
|
397
|
+
|
|
398
|
+
if (newUsers.length < _limit) {
|
|
399
|
+
_hasMore = false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
final currentUsers = state.value ?? [];
|
|
403
|
+
final updatedUsers = _page == 1 ? newUsers : [...currentUsers, ...newUsers];
|
|
404
|
+
|
|
405
|
+
state = AsyncData(updatedUsers);
|
|
406
|
+
_page++;
|
|
407
|
+
|
|
408
|
+
return updatedUsers;
|
|
409
|
+
} catch (error, stackTrace) {
|
|
410
|
+
state = AsyncError(error, stackTrace);
|
|
411
|
+
rethrow;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
Future<void> loadMoreUsers() async {
|
|
416
|
+
await _loadUsers();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
Future<void> refresh() async {
|
|
420
|
+
_page = 1;
|
|
421
|
+
_hasMore = true;
|
|
422
|
+
await _loadUsers();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// State management with Notifier for single user
|
|
427
|
+
final userNotifierProvider = AsyncNotifierProvider.family<UserNotifier, User, String>(UserNotifier.new);
|
|
428
|
+
|
|
429
|
+
class UserNotifier extends FamilyAsyncNotifier<User, String> {
|
|
430
|
+
@override
|
|
431
|
+
Future<User> build(String arg) async {
|
|
432
|
+
final repository = ref.read(userRepositoryProvider);
|
|
433
|
+
return repository.getUser(arg);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
Future<void> updateUser(User user) async {
|
|
437
|
+
state = const AsyncLoading();
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
final repository = ref.read(userRepositoryProvider);
|
|
441
|
+
final updatedUser = await repository.updateUser(user);
|
|
442
|
+
state = AsyncData(updatedUser);
|
|
443
|
+
} catch (error, stackTrace) {
|
|
444
|
+
state = AsyncError(error, stackTrace);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// App-wide state providers
|
|
450
|
+
final appThemeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
|
|
451
|
+
final appLocaleProvider = StateProvider<Locale>((ref) => const Locale('en', 'US'));
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Modern Flutter UI with Material 3
|
|
455
|
+
|
|
456
|
+
```dart
|
|
457
|
+
// Modern app with Material 3 and go_router
|
|
458
|
+
class MyApp extends ConsumerWidget {
|
|
459
|
+
const MyApp({super.key});
|
|
460
|
+
|
|
461
|
+
@override
|
|
462
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
463
|
+
final appRouter = ref.watch(goRouterProvider);
|
|
464
|
+
final themeMode = ref.watch(appThemeProvider);
|
|
465
|
+
final appLocale = ref.watch(appLocaleProvider);
|
|
466
|
+
|
|
467
|
+
return MaterialApp.router(
|
|
468
|
+
title: 'My Flutter App',
|
|
469
|
+
debugShowCheckedModeBanner: false,
|
|
470
|
+
themeMode: themeMode,
|
|
471
|
+
theme: AppTheme.lightTheme,
|
|
472
|
+
darkTheme: AppTheme.darkTheme,
|
|
473
|
+
locale: appLocale,
|
|
474
|
+
supportedLocales: const [
|
|
475
|
+
Locale('en', 'US'),
|
|
476
|
+
Locale('es', 'ES'),
|
|
477
|
+
Locale('fr', 'FR'),
|
|
478
|
+
],
|
|
479
|
+
localizationsDelegates: const [
|
|
480
|
+
AppLocalizations.delegate,
|
|
481
|
+
GlobalMaterialLocalizations.delegate,
|
|
482
|
+
GlobalWidgetsLocalizations.delegate,
|
|
483
|
+
GlobalCupertinoLocalizations.delegate,
|
|
484
|
+
],
|
|
485
|
+
routerConfig: appRouter,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Modern theme configuration
|
|
491
|
+
class AppTheme {
|
|
492
|
+
static ThemeData get lightTheme {
|
|
493
|
+
return ThemeData(
|
|
494
|
+
useMaterial3: true,
|
|
495
|
+
colorScheme: ColorScheme.fromSeed(
|
|
496
|
+
seedColor: const Color(0xFF6750A4),
|
|
497
|
+
brightness: Brightness.light,
|
|
498
|
+
),
|
|
499
|
+
appBarTheme: const AppBarTheme(
|
|
500
|
+
centerTitle: true,
|
|
501
|
+
elevation: 0,
|
|
502
|
+
scrolledUnderElevation: 1,
|
|
503
|
+
),
|
|
504
|
+
cardTheme: CardTheme(
|
|
505
|
+
elevation: 2,
|
|
506
|
+
shape: RoundedRectangleBorder(
|
|
507
|
+
borderRadius: BorderRadius.circular(16),
|
|
508
|
+
),
|
|
509
|
+
),
|
|
510
|
+
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
511
|
+
style: ElevatedButton.styleFrom(
|
|
512
|
+
elevation: 1,
|
|
513
|
+
shape: RoundedRectangleBorder(
|
|
514
|
+
borderRadius: BorderRadius.circular(20),
|
|
515
|
+
),
|
|
516
|
+
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
inputDecorationTheme: InputDecorationTheme(
|
|
520
|
+
border: OutlineInputBorder(
|
|
521
|
+
borderRadius: BorderRadius.circular(12),
|
|
522
|
+
),
|
|
523
|
+
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
524
|
+
),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
static ThemeData get darkTheme {
|
|
529
|
+
return ThemeData(
|
|
530
|
+
useMaterial3: true,
|
|
531
|
+
colorScheme: ColorScheme.fromSeed(
|
|
532
|
+
seedColor: const Color(0xFF6750A4),
|
|
533
|
+
brightness: Brightness.dark,
|
|
534
|
+
),
|
|
535
|
+
appBarTheme: const AppBarTheme(
|
|
536
|
+
centerTitle: true,
|
|
537
|
+
elevation: 0,
|
|
538
|
+
scrolledUnderElevation: 1,
|
|
539
|
+
),
|
|
540
|
+
cardTheme: CardTheme(
|
|
541
|
+
elevation: 2,
|
|
542
|
+
shape: RoundedRectangleBorder(
|
|
543
|
+
borderRadius: BorderRadius.circular(16),
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Modern user list widget with state management
|
|
551
|
+
class UserListScreen extends ConsumerWidget {
|
|
552
|
+
const UserListScreen({super.key});
|
|
553
|
+
|
|
554
|
+
@override
|
|
555
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
556
|
+
final usersAsync = ref.watch(usersProvider);
|
|
557
|
+
|
|
558
|
+
return Scaffold(
|
|
559
|
+
appBar: AppBar(
|
|
560
|
+
title: const Text('Users'),
|
|
561
|
+
actions: [
|
|
562
|
+
IconButton(
|
|
563
|
+
icon: const Icon(Icons.refresh),
|
|
564
|
+
onPressed: () {
|
|
565
|
+
ref.read(usersProvider.notifier).refresh();
|
|
566
|
+
},
|
|
567
|
+
),
|
|
568
|
+
],
|
|
569
|
+
),
|
|
570
|
+
body: RefreshIndicator(
|
|
571
|
+
onRefresh: () async {
|
|
572
|
+
await ref.read(usersProvider.notifier).refresh();
|
|
573
|
+
},
|
|
574
|
+
child: usersAsync.when(
|
|
575
|
+
data: (users) {
|
|
576
|
+
if (users.isEmpty) {
|
|
577
|
+
return const EmptyStateWidget(
|
|
578
|
+
message: 'No users found',
|
|
579
|
+
icon: Icons.people_outline,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return NotificationListener<ScrollNotification>(
|
|
584
|
+
onNotification: (scrollInfo) {
|
|
585
|
+
if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
|
|
586
|
+
ref.read(usersProvider.notifier).loadMoreUsers();
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
},
|
|
590
|
+
child: ListView.builder(
|
|
591
|
+
padding: const EdgeInsets.all(16),
|
|
592
|
+
itemCount: users.length + 1, // +1 for loading indicator
|
|
593
|
+
itemBuilder: (context, index) {
|
|
594
|
+
if (index == users.length) {
|
|
595
|
+
return const LoadingIndicator();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return UserCard(user: users[index]);
|
|
599
|
+
},
|
|
600
|
+
),
|
|
601
|
+
);
|
|
602
|
+
},
|
|
603
|
+
loading: () => const Center(child: CircularProgressIndicator()),
|
|
604
|
+
error: (error, stack) => ErrorWidget(
|
|
605
|
+
error: error,
|
|
606
|
+
onRetry: () {
|
|
607
|
+
ref.read(usersProvider.notifier).refresh();
|
|
608
|
+
},
|
|
609
|
+
),
|
|
610
|
+
),
|
|
611
|
+
),
|
|
612
|
+
floatingActionButton: FloatingActionButton.extended(
|
|
613
|
+
onPressed: () {
|
|
614
|
+
context.go('/add-user');
|
|
615
|
+
},
|
|
616
|
+
icon: const Icon(Icons.add),
|
|
617
|
+
label: const Text('Add User'),
|
|
618
|
+
),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Modern user card widget
|
|
624
|
+
class UserCard extends ConsumerWidget {
|
|
625
|
+
final User user;
|
|
626
|
+
|
|
627
|
+
const UserCard({
|
|
628
|
+
super.key,
|
|
629
|
+
required this.user,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
@override
|
|
633
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
634
|
+
return Card(
|
|
635
|
+
margin: const EdgeInsets.only(bottom: 12),
|
|
636
|
+
child: InkWell(
|
|
637
|
+
onTap: () {
|
|
638
|
+
context.go('/users/${user.id}');
|
|
639
|
+
},
|
|
640
|
+
borderRadius: BorderRadius.circular(16),
|
|
641
|
+
child: Padding(
|
|
642
|
+
padding: const EdgeInsets.all(16),
|
|
643
|
+
child: Row(
|
|
644
|
+
children: [
|
|
645
|
+
CircleAvatar(
|
|
646
|
+
radius: 28,
|
|
647
|
+
backgroundImage: NetworkImage(user.avatarUrl),
|
|
648
|
+
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
|
649
|
+
child: user.avatarUrl.isEmpty
|
|
650
|
+
? Text(
|
|
651
|
+
user.name.isNotEmpty ? user.name[0].toUpperCase() : '?',
|
|
652
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
653
|
+
)
|
|
654
|
+
: null,
|
|
655
|
+
),
|
|
656
|
+
const SizedBox(width: 16),
|
|
657
|
+
Expanded(
|
|
658
|
+
child: Column(
|
|
659
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
660
|
+
children: [
|
|
661
|
+
Text(
|
|
662
|
+
user.name,
|
|
663
|
+
style: Theme.of(context).textTheme.titleMedium,
|
|
664
|
+
),
|
|
665
|
+
const SizedBox(height: 4),
|
|
666
|
+
Text(
|
|
667
|
+
user.email,
|
|
668
|
+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
669
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
670
|
+
),
|
|
671
|
+
),
|
|
672
|
+
],
|
|
673
|
+
),
|
|
674
|
+
),
|
|
675
|
+
IconButton(
|
|
676
|
+
icon: const Icon(Icons.more_vert),
|
|
677
|
+
onPressed: () {
|
|
678
|
+
_showUserMenu(context, ref, user);
|
|
679
|
+
},
|
|
680
|
+
),
|
|
681
|
+
],
|
|
682
|
+
),
|
|
683
|
+
),
|
|
684
|
+
),
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
void _showUserMenu(BuildContext context, WidgetRef ref, User user) {
|
|
689
|
+
showModalBottomSheet(
|
|
690
|
+
context: context,
|
|
691
|
+
builder: (context) {
|
|
692
|
+
return SafeArea(
|
|
693
|
+
child: Column(
|
|
694
|
+
mainAxisSize: MainAxisSize.min,
|
|
695
|
+
children: [
|
|
696
|
+
ListTile(
|
|
697
|
+
leading: const Icon(Icons.edit),
|
|
698
|
+
title: const Text('Edit User'),
|
|
699
|
+
onTap: () {
|
|
700
|
+
Navigator.pop(context);
|
|
701
|
+
context.go('/users/${user.id}/edit');
|
|
702
|
+
},
|
|
703
|
+
),
|
|
704
|
+
ListTile(
|
|
705
|
+
leading: const Icon(Icons.delete, color: Colors.red),
|
|
706
|
+
title: const Text('Delete User', style: TextStyle(color: Colors.red)),
|
|
707
|
+
onTap: () async {
|
|
708
|
+
Navigator.pop(context);
|
|
709
|
+
|
|
710
|
+
final confirmed = await _showDeleteConfirmation(context);
|
|
711
|
+
if (confirmed) {
|
|
712
|
+
try {
|
|
713
|
+
await ref.read(userRepositoryProvider).deleteUser(user.id);
|
|
714
|
+
if (context.mounted) {
|
|
715
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
716
|
+
const SnackBar(content: Text('User deleted successfully')),
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (context.mounted) {
|
|
721
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
722
|
+
SnackBar(content: Text('Failed to delete user: $error')),
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
),
|
|
729
|
+
],
|
|
730
|
+
),
|
|
731
|
+
);
|
|
732
|
+
},
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
Future<bool> _showDeleteConfirmation(BuildContext context) async {
|
|
737
|
+
return await showDialog<bool>(
|
|
738
|
+
context: context,
|
|
739
|
+
builder: (context) {
|
|
740
|
+
return AlertDialog(
|
|
741
|
+
title: const Text('Delete User'),
|
|
742
|
+
content: Text('Are you sure you want to delete ${user.name}?'),
|
|
743
|
+
actions: [
|
|
744
|
+
TextButton(
|
|
745
|
+
onPressed: () => Navigator.pop(context, false),
|
|
746
|
+
child: const Text('Cancel'),
|
|
747
|
+
),
|
|
748
|
+
TextButton(
|
|
749
|
+
onPressed: () => Navigator.pop(context, true),
|
|
750
|
+
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
751
|
+
child: const Text('Delete'),
|
|
752
|
+
),
|
|
753
|
+
],
|
|
754
|
+
);
|
|
755
|
+
},
|
|
756
|
+
) ?? false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Go Router for Navigation
|
|
762
|
+
|
|
763
|
+
```dart
|
|
764
|
+
// Go router configuration
|
|
765
|
+
final goRouterProvider = Provider<GoRouter>((ref) {
|
|
766
|
+
return GoRouter(
|
|
767
|
+
initialLocation: '/',
|
|
768
|
+
debugLogDiagnostics: AppConfig.isDebug,
|
|
769
|
+
routes: [
|
|
770
|
+
// Shell route for navigation
|
|
771
|
+
ShellRoute(
|
|
772
|
+
builder: (context, state, child) {
|
|
773
|
+
return MainScaffold(child: child);
|
|
774
|
+
},
|
|
775
|
+
routes: [
|
|
776
|
+
// Home route
|
|
777
|
+
GoRoute(
|
|
778
|
+
path: '/',
|
|
779
|
+
builder: (context, state) => const HomeScreen(),
|
|
780
|
+
),
|
|
781
|
+
|
|
782
|
+
// User routes
|
|
783
|
+
GoRoute(
|
|
784
|
+
path: '/users',
|
|
785
|
+
builder: (context, state) => const UserListScreen(),
|
|
786
|
+
routes: [
|
|
787
|
+
GoRoute(
|
|
788
|
+
path: '/:userId',
|
|
789
|
+
builder: (context, state) {
|
|
790
|
+
final userId = state.pathParameters['userId']!;
|
|
791
|
+
return UserDetailScreen(userId: userId);
|
|
792
|
+
},
|
|
793
|
+
routes: [
|
|
794
|
+
GoRoute(
|
|
795
|
+
path: '/edit',
|
|
796
|
+
builder: (context, state) {
|
|
797
|
+
final userId = state.pathParameters['userId']!;
|
|
798
|
+
return EditUserScreen(userId: userId);
|
|
799
|
+
},
|
|
800
|
+
),
|
|
801
|
+
],
|
|
802
|
+
),
|
|
803
|
+
],
|
|
804
|
+
),
|
|
805
|
+
|
|
806
|
+
// Settings route
|
|
807
|
+
GoRoute(
|
|
808
|
+
path: '/settings',
|
|
809
|
+
builder: (context, state) => const SettingsScreen(),
|
|
810
|
+
routes: [
|
|
811
|
+
GoRoute(
|
|
812
|
+
path: '/profile',
|
|
813
|
+
builder: (context, state) => const ProfileSettingsScreen(),
|
|
814
|
+
),
|
|
815
|
+
GoRoute(
|
|
816
|
+
path: '/appearance',
|
|
817
|
+
builder: (context, state) => const AppearanceSettingsScreen(),
|
|
818
|
+
),
|
|
819
|
+
],
|
|
820
|
+
),
|
|
821
|
+
],
|
|
822
|
+
),
|
|
823
|
+
|
|
824
|
+
// Standalone routes
|
|
825
|
+
GoRoute(
|
|
826
|
+
path: '/add-user',
|
|
827
|
+
builder: (context, state) => const AddUserScreen(),
|
|
828
|
+
),
|
|
829
|
+
|
|
830
|
+
GoRoute(
|
|
831
|
+
path: '/login',
|
|
832
|
+
builder: (context, state) => const LoginScreen(),
|
|
833
|
+
),
|
|
834
|
+
],
|
|
835
|
+
|
|
836
|
+
// Error handling
|
|
837
|
+
errorBuilder: (context, state) => ErrorScreen(error: state.error),
|
|
838
|
+
|
|
839
|
+
// Redirects
|
|
840
|
+
redirect: (context, state) {
|
|
841
|
+
// Example: redirect to login if not authenticated
|
|
842
|
+
final isAuthenticated = true; // Check authentication status
|
|
843
|
+
|
|
844
|
+
if (!isAuthenticated && !state.location.startsWith('/login')) {
|
|
845
|
+
return '/login';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return null;
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Main scaffold with bottom navigation
|
|
854
|
+
class MainScaffold extends ConsumerWidget {
|
|
855
|
+
const MainScaffold({
|
|
856
|
+
required this.child,
|
|
857
|
+
super.key,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
final Widget child;
|
|
861
|
+
|
|
862
|
+
@override
|
|
863
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
864
|
+
final selectedIndex = ref.watch(bottomNavigationIndexProvider);
|
|
865
|
+
|
|
866
|
+
return Scaffold(
|
|
867
|
+
body: child,
|
|
868
|
+
bottomNavigationBar: NavigationBar(
|
|
869
|
+
selectedIndex: selectedIndex,
|
|
870
|
+
onDestinationSelected: (index) {
|
|
871
|
+
ref.read(bottomNavigationIndexProvider.notifier).state = index;
|
|
872
|
+
|
|
873
|
+
switch (index) {
|
|
874
|
+
case 0:
|
|
875
|
+
context.go('/');
|
|
876
|
+
break;
|
|
877
|
+
case 1:
|
|
878
|
+
context.go('/users');
|
|
879
|
+
break;
|
|
880
|
+
case 2:
|
|
881
|
+
context.go('/settings');
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
destinations: const [
|
|
886
|
+
NavigationDestination(
|
|
887
|
+
icon: Icon(Icons.home_outlined),
|
|
888
|
+
selectedIcon: Icon(Icons.home),
|
|
889
|
+
label: 'Home',
|
|
890
|
+
),
|
|
891
|
+
NavigationDestination(
|
|
892
|
+
icon: Icon(Icons.people_outlined),
|
|
893
|
+
selectedIcon: Icon(Icons.people),
|
|
894
|
+
label: 'Users',
|
|
895
|
+
),
|
|
896
|
+
NavigationDestination(
|
|
897
|
+
icon: Icon(Icons.settings_outlined),
|
|
898
|
+
selectedIcon: Icon(Icons.settings),
|
|
899
|
+
label: 'Settings',
|
|
900
|
+
),
|
|
901
|
+
],
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Provider for bottom navigation state
|
|
908
|
+
final bottomNavigationIndexProvider = StateProvider<int>((ref) => 0);
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
## Performance Considerations
|
|
912
|
+
|
|
913
|
+
### Widget Performance
|
|
914
|
+
|
|
915
|
+
```dart
|
|
916
|
+
// Performance-optimized widgets with const constructors
|
|
917
|
+
class OptimizedUserCard extends StatelessWidget {
|
|
918
|
+
final User user;
|
|
919
|
+
final VoidCallback? onTap;
|
|
920
|
+
final VoidCallback? onEdit;
|
|
921
|
+
final VoidCallback? onDelete;
|
|
922
|
+
|
|
923
|
+
const OptimizedUserCard({
|
|
924
|
+
super.key,
|
|
925
|
+
required this.user,
|
|
926
|
+
this.onTap,
|
|
927
|
+
this.onEdit,
|
|
928
|
+
this.onDelete,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
@override
|
|
932
|
+
Widget build(BuildContext context) {
|
|
933
|
+
return Card(
|
|
934
|
+
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
935
|
+
child: InkWell(
|
|
936
|
+
onTap: onTap,
|
|
937
|
+
borderRadius: BorderRadius.circular(12),
|
|
938
|
+
child: Padding(
|
|
939
|
+
padding: const EdgeInsets.all(16),
|
|
940
|
+
child: Row(
|
|
941
|
+
children: [
|
|
942
|
+
// Use Hero widget for smooth avatar transitions
|
|
943
|
+
Hero(
|
|
944
|
+
tag: 'user-avatar-${user.id}',
|
|
945
|
+
child: UserAvatar(
|
|
946
|
+
imageUrl: user.avatarUrl,
|
|
947
|
+
name: user.name,
|
|
948
|
+
size: 48,
|
|
949
|
+
),
|
|
950
|
+
),
|
|
951
|
+
const SizedBox(width: 16),
|
|
952
|
+
Expanded(
|
|
953
|
+
child: _buildUserInfo(),
|
|
954
|
+
),
|
|
955
|
+
_buildActionButtons(),
|
|
956
|
+
],
|
|
957
|
+
),
|
|
958
|
+
),
|
|
959
|
+
),
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
Widget _buildUserInfo() {
|
|
964
|
+
return Column(
|
|
965
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
966
|
+
mainAxisSize: MainAxisSize.min,
|
|
967
|
+
children: [
|
|
968
|
+
Text(
|
|
969
|
+
user.name,
|
|
970
|
+
style: const TextStyle(
|
|
971
|
+
fontSize: 16,
|
|
972
|
+
fontWeight: FontWeight.w600,
|
|
973
|
+
),
|
|
974
|
+
maxLines: 1,
|
|
975
|
+
overflow: TextOverflow.ellipsis,
|
|
976
|
+
),
|
|
977
|
+
const SizedBox(height: 4),
|
|
978
|
+
Text(
|
|
979
|
+
user.email,
|
|
980
|
+
style: const TextStyle(
|
|
981
|
+
fontSize: 14,
|
|
982
|
+
color: Colors.grey,
|
|
983
|
+
),
|
|
984
|
+
maxLines: 1,
|
|
985
|
+
overflow: TextOverflow.ellipsis,
|
|
986
|
+
),
|
|
987
|
+
],
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
Widget _buildActionButtons() {
|
|
992
|
+
return Row(
|
|
993
|
+
mainAxisSize: MainAxisSize.min,
|
|
994
|
+
children: [
|
|
995
|
+
if (onEdit != null)
|
|
996
|
+
IconButton(
|
|
997
|
+
icon: const Icon(Icons.edit_outlined),
|
|
998
|
+
onPressed: onEdit,
|
|
999
|
+
visualDensity: VisualDensity.compact,
|
|
1000
|
+
),
|
|
1001
|
+
if (onDelete != null)
|
|
1002
|
+
IconButton(
|
|
1003
|
+
icon: const Icon(Icons.delete_outline),
|
|
1004
|
+
onPressed: onDelete,
|
|
1005
|
+
visualDensity: VisualDensity.compact,
|
|
1006
|
+
),
|
|
1007
|
+
],
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Efficient image loading and caching
|
|
1013
|
+
class CachedNetworkImage extends StatefulWidget {
|
|
1014
|
+
final String imageUrl;
|
|
1015
|
+
final double? width;
|
|
1016
|
+
final double? height;
|
|
1017
|
+
final Widget? placeholder;
|
|
1018
|
+
final Widget? errorWidget;
|
|
1019
|
+
final BoxFit fit;
|
|
1020
|
+
|
|
1021
|
+
const CachedNetworkImage({
|
|
1022
|
+
super.key,
|
|
1023
|
+
required this.imageUrl,
|
|
1024
|
+
this.width,
|
|
1025
|
+
this.height,
|
|
1026
|
+
this.placeholder,
|
|
1027
|
+
this.errorWidget,
|
|
1028
|
+
this.fit = BoxFit.cover,
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
@override
|
|
1032
|
+
State<CachedNetworkImage> createState() => _CachedNetworkImageState();
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
class _CachedNetworkImageState extends State<CachedNetworkImage> {
|
|
1036
|
+
final Map<String, ui.Image> _imageCache = {};
|
|
1037
|
+
bool _isLoading = true;
|
|
1038
|
+
bool _hasError = false;
|
|
1039
|
+
|
|
1040
|
+
@override
|
|
1041
|
+
void initState() {
|
|
1042
|
+
super.initState();
|
|
1043
|
+
_loadImage();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
Future<void> _loadImage() async {
|
|
1047
|
+
if (_imageCache.containsKey(widget.imageUrl)) {
|
|
1048
|
+
if (mounted) {
|
|
1049
|
+
setState(() {
|
|
1050
|
+
_isLoading = false;
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
try {
|
|
1057
|
+
final image = await _fetchImage();
|
|
1058
|
+
_imageCache[widget.imageUrl] = image;
|
|
1059
|
+
|
|
1060
|
+
if (mounted) {
|
|
1061
|
+
setState(() {
|
|
1062
|
+
_isLoading = false;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
if (mounted) {
|
|
1067
|
+
setState(() {
|
|
1068
|
+
_isLoading = false;
|
|
1069
|
+
_hasError = true;
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
Future<ui.Image> _fetchImage() async {
|
|
1076
|
+
final completer = Completer<ui.Image>();
|
|
1077
|
+
final codec = await ui.instantiateImageCodec(
|
|
1078
|
+
await NetworkAssetBundle(Uri.parse(widget.imageUrl)).load(widget.imageUrl).then((bytes) => bytes.buffer.asUint8List()),
|
|
1079
|
+
);
|
|
1080
|
+
final frame = await codec.getNextFrame();
|
|
1081
|
+
completer.complete(frame.image);
|
|
1082
|
+
return completer.future;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
@override
|
|
1086
|
+
Widget build(BuildContext context) {
|
|
1087
|
+
if (_isLoading) {
|
|
1088
|
+
return widget.placeholder ??
|
|
1089
|
+
Container(
|
|
1090
|
+
width: widget.width,
|
|
1091
|
+
height: widget.height,
|
|
1092
|
+
color: Colors.grey[200],
|
|
1093
|
+
child: const Center(
|
|
1094
|
+
child: CircularProgressIndicator(),
|
|
1095
|
+
),
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (_hasError) {
|
|
1100
|
+
return widget.errorWidget ??
|
|
1101
|
+
Container(
|
|
1102
|
+
width: widget.width,
|
|
1103
|
+
height: widget.height,
|
|
1104
|
+
color: Colors.grey[200],
|
|
1105
|
+
child: const Icon(Icons.error),
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
final image = _imageCache[widget.imageUrl];
|
|
1110
|
+
return CustomPaint(
|
|
1111
|
+
size: Size(widget.width ?? double.infinity, widget.height ?? double.infinity),
|
|
1112
|
+
painter: _ImagePainter(image: image!, fit: widget.fit),
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
class _ImagePainter extends CustomPainter {
|
|
1118
|
+
final ui.Image image;
|
|
1119
|
+
final BoxFit fit;
|
|
1120
|
+
|
|
1121
|
+
_ImagePainter({required this.image, required this.fit});
|
|
1122
|
+
|
|
1123
|
+
@override
|
|
1124
|
+
void paint(Canvas canvas, Size size) {
|
|
1125
|
+
final imageSize = Size(image.width.toDouble(), image.height.toDouble());
|
|
1126
|
+
final scales = _calculateScales(imageSize, size);
|
|
1127
|
+
|
|
1128
|
+
final paint = Paint()
|
|
1129
|
+
..isAntiAlias = true
|
|
1130
|
+
..filterQuality = FilterQuality.high;
|
|
1131
|
+
|
|
1132
|
+
canvas.save();
|
|
1133
|
+
|
|
1134
|
+
if (fit == BoxFit.cover) {
|
|
1135
|
+
canvas.scale(scales.dx, scales.dy);
|
|
1136
|
+
canvas.drawImageRect(
|
|
1137
|
+
image,
|
|
1138
|
+
Rect.fromLTWH(0, 0, imageSize.width, imageSize.height),
|
|
1139
|
+
Rect.fromLTWH(0, 0, size.width / scales.dx, size.height / scales.dy),
|
|
1140
|
+
paint,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
canvas.restore();
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
Offset _calculateScales(Size inputSize, Size outputSize) {
|
|
1148
|
+
final scaleX = outputSize.width / inputSize.width;
|
|
1149
|
+
final scaleY = outputSize.height / inputSize.height;
|
|
1150
|
+
return Offset(scaleX, scaleY);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
@override
|
|
1154
|
+
bool shouldRepaint(covariant _ImagePainter oldDelegate) {
|
|
1155
|
+
return image != oldDelegate.image || fit != oldDelegate.fit;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// ListView with lazy loading and recycling
|
|
1160
|
+
class OptimizedListView<T> extends StatelessWidget {
|
|
1161
|
+
final List<T> items;
|
|
1162
|
+
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
|
1163
|
+
final VoidCallback? onLoadMore;
|
|
1164
|
+
final bool hasMore;
|
|
1165
|
+
final bool isLoading;
|
|
1166
|
+
|
|
1167
|
+
const OptimizedListView({
|
|
1168
|
+
super.key,
|
|
1169
|
+
required this.items,
|
|
1170
|
+
required this.itemBuilder,
|
|
1171
|
+
this.onLoadMore,
|
|
1172
|
+
this.hasMore = false,
|
|
1173
|
+
this.isLoading = false,
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
@override
|
|
1177
|
+
Widget build(BuildContext context) {
|
|
1178
|
+
return NotificationListener<ScrollNotification>(
|
|
1179
|
+
onNotification: (notification) {
|
|
1180
|
+
if (notification is ScrollEndNotification &&
|
|
1181
|
+
notification.metrics.extentAfter == 0 &&
|
|
1182
|
+
hasMore &&
|
|
1183
|
+
!isLoading &&
|
|
1184
|
+
onLoadMore != null) {
|
|
1185
|
+
onLoadMore!();
|
|
1186
|
+
}
|
|
1187
|
+
return false;
|
|
1188
|
+
},
|
|
1189
|
+
child: ListView.builder(
|
|
1190
|
+
itemCount: items.length + (hasMore ? 1 : 0),
|
|
1191
|
+
itemBuilder: (context, index) {
|
|
1192
|
+
if (index == items.length) {
|
|
1193
|
+
return const Center(
|
|
1194
|
+
child: Padding(
|
|
1195
|
+
padding: EdgeInsets.all(16.0),
|
|
1196
|
+
child: CircularProgressIndicator(),
|
|
1197
|
+
),
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return itemBuilder(context, items[index], index);
|
|
1202
|
+
},
|
|
1203
|
+
),
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
### Memory Management
|
|
1210
|
+
|
|
1211
|
+
```dart
|
|
1212
|
+
// Efficient memory usage with ImageCache
|
|
1213
|
+
class ImageCacheManager {
|
|
1214
|
+
static final ImageCacheManager _instance = ImageCacheManager._internal();
|
|
1215
|
+
factory ImageCacheManager() => _instance;
|
|
1216
|
+
ImageCacheManager._internal();
|
|
1217
|
+
|
|
1218
|
+
final PaintingBinding _paintingBinding = PaintingBinding.instance;
|
|
1219
|
+
final Map<String, ui.Image> _memoryCache = {};
|
|
1220
|
+
final int _maxCacheSize = 100 * 1024 * 1024; // 100MB
|
|
1221
|
+
int _currentCacheSize = 0;
|
|
1222
|
+
|
|
1223
|
+
Future<ui.Image?> getImage(String url) async {
|
|
1224
|
+
// Check memory cache first
|
|
1225
|
+
if (_memoryCache.containsKey(url)) {
|
|
1226
|
+
return _memoryCache[url];
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Check painting binding cache
|
|
1230
|
+
final cachedImage = _paintingBinding.imageCache?.image;
|
|
1231
|
+
if (cachedImage != null) {
|
|
1232
|
+
return cachedImage;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
try {
|
|
1236
|
+
final image = await _loadImage(url);
|
|
1237
|
+
_addToMemoryCache(url, image);
|
|
1238
|
+
return image;
|
|
1239
|
+
} catch (e) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
Future<ui.Image> _loadImage(String url) async {
|
|
1245
|
+
final completer = Completer<ui.Image>();
|
|
1246
|
+
final codec = await ui.instantiateImageCodec(
|
|
1247
|
+
await _fetchImageData(url),
|
|
1248
|
+
);
|
|
1249
|
+
final frame = await codec.getNextFrame();
|
|
1250
|
+
completer.complete(frame.image);
|
|
1251
|
+
return completer.future;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
Future<Uint8List> _fetchImageData(String url) async {
|
|
1255
|
+
final response = await http.get(Uri.parse(url));
|
|
1256
|
+
return response.bodyBytes;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
void _addToMemoryCache(String url, ui.Image image) {
|
|
1260
|
+
final imageSize = image.width * image.height * 4; // 4 bytes per pixel
|
|
1261
|
+
|
|
1262
|
+
if (_currentCacheSize + imageSize > _maxCacheSize) {
|
|
1263
|
+
_evictLeastRecentlyUsed(imageSize);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
_memoryCache[url] = image;
|
|
1267
|
+
_currentCacheSize += imageSize;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
void _evictLeastRecentlyUsed(int requiredSize) {
|
|
1271
|
+
final entries = _memoryCache.entries.toList();
|
|
1272
|
+
entries.sort((a, b) => a.key.compareTo(b.key));
|
|
1273
|
+
|
|
1274
|
+
int freedSize = 0;
|
|
1275
|
+
for (final entry in entries) {
|
|
1276
|
+
final imageSize = entry.value.width * entry.value.height * 4;
|
|
1277
|
+
_memoryCache.remove(entry.key);
|
|
1278
|
+
freedSize += imageSize;
|
|
1279
|
+
_currentCacheSize -= imageSize;
|
|
1280
|
+
|
|
1281
|
+
if (freedSize >= requiredSize) {
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
void clearCache() {
|
|
1288
|
+
_memoryCache.clear();
|
|
1289
|
+
_currentCacheSize = 0;
|
|
1290
|
+
_paintingBinding.imageCache?.clear();
|
|
1291
|
+
_paintingBinding.imageCache?.clearLiveImages();
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Resource management with automatic cleanup
|
|
1296
|
+
class ResourceManager {
|
|
1297
|
+
final Map<String, StreamSubscription> _subscriptions = {};
|
|
1298
|
+
final Map<String, Timer> _timers = {};
|
|
1299
|
+
|
|
1300
|
+
StreamSubscription<T>? addSubscription<T>(
|
|
1301
|
+
String key,
|
|
1302
|
+
StreamSubscription<T> subscription,
|
|
1303
|
+
) {
|
|
1304
|
+
_subscriptions[key] = subscription as StreamSubscription;
|
|
1305
|
+
return subscription;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
Timer? addTimer(String key, Duration duration, VoidCallback callback) {
|
|
1309
|
+
final timer = Timer(duration, callback);
|
|
1310
|
+
_timers[key] = timer;
|
|
1311
|
+
return timer;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
void removeSubscription(String key) {
|
|
1315
|
+
final subscription = _subscriptions.remove(key);
|
|
1316
|
+
subscription?.cancel();
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
void removeTimer(String key) {
|
|
1320
|
+
final timer = _timers.remove(key);
|
|
1321
|
+
timer?.cancel();
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
void dispose() {
|
|
1325
|
+
for (final subscription in _subscriptions.values) {
|
|
1326
|
+
subscription.cancel();
|
|
1327
|
+
}
|
|
1328
|
+
_subscriptions.clear();
|
|
1329
|
+
|
|
1330
|
+
for (final timer in _timers.values) {
|
|
1331
|
+
timer.cancel();
|
|
1332
|
+
}
|
|
1333
|
+
_timers.clear();
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Widget with automatic resource cleanup
|
|
1338
|
+
class AutoCleanupWidget extends StatefulWidget {
|
|
1339
|
+
final Widget child;
|
|
1340
|
+
final VoidCallback? onInit;
|
|
1341
|
+
final VoidCallback? onDispose;
|
|
1342
|
+
|
|
1343
|
+
const AutoCleanupWidget({
|
|
1344
|
+
super.key,
|
|
1345
|
+
required this.child,
|
|
1346
|
+
this.onInit,
|
|
1347
|
+
this.onDispose,
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
@override
|
|
1351
|
+
State<AutoCleanupWidget> createState() => _AutoCleanupWidgetState();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
class _AutoCleanupWidgetState extends State<AutoCleanupWidget> {
|
|
1355
|
+
final ResourceManager _resourceManager = ResourceManager();
|
|
1356
|
+
|
|
1357
|
+
@override
|
|
1358
|
+
void initState() {
|
|
1359
|
+
super.initState();
|
|
1360
|
+
widget.onInit?.call();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
@override
|
|
1364
|
+
void dispose() {
|
|
1365
|
+
_resourceManager.dispose();
|
|
1366
|
+
widget.onDispose?.call();
|
|
1367
|
+
super.dispose();
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
@override
|
|
1371
|
+
Widget build(BuildContext context) {
|
|
1372
|
+
return widget.child;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
```
|
|
1376
|
+
|
|
1377
|
+
## Testing Strategy
|
|
1378
|
+
|
|
1379
|
+
### Unit Testing with Dart Test Framework
|
|
1380
|
+
|
|
1381
|
+
```dart
|
|
1382
|
+
// Unit tests for business logic
|
|
1383
|
+
void main() {
|
|
1384
|
+
group('UserRepository', () {
|
|
1385
|
+
late UserRepository userRepository;
|
|
1386
|
+
late MockDio mockDio;
|
|
1387
|
+
|
|
1388
|
+
setUp(() {
|
|
1389
|
+
mockDio = MockDio();
|
|
1390
|
+
userRepository = HttpUserRepository(mockDio);
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
test('should return user when getUser is called with valid ID', () async {
|
|
1394
|
+
// Arrange
|
|
1395
|
+
final userId = '123';
|
|
1396
|
+
final userJson = {
|
|
1397
|
+
'id': userId,
|
|
1398
|
+
'name': 'John Doe',
|
|
1399
|
+
'email': 'john@example.com',
|
|
1400
|
+
'avatarUrl': 'https://example.com/avatar.jpg',
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
when(() => mockDio.get('/users/$userId'))
|
|
1404
|
+
.thenAnswer((_) async => Response(data: userJson, statusCode: 200));
|
|
1405
|
+
|
|
1406
|
+
// Act
|
|
1407
|
+
final result = await userRepository.getUser(userId);
|
|
1408
|
+
|
|
1409
|
+
// Assert
|
|
1410
|
+
expect(result.id, equals(userId));
|
|
1411
|
+
expect(result.name, equals('John Doe'));
|
|
1412
|
+
expect(result.email, equals('john@example.com'));
|
|
1413
|
+
expect(result.avatarUrl, equals('https://example.com/avatar.jpg'));
|
|
1414
|
+
|
|
1415
|
+
verify(() => mockDio.get('/users/$userId')).called(1);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
test('should throw UserRepositoryException when API call fails', () async {
|
|
1419
|
+
// Arrange
|
|
1420
|
+
final userId = '123';
|
|
1421
|
+
|
|
1422
|
+
when(() => mockDio.get('/users/$userId'))
|
|
1423
|
+
.thenThrow(DioException(requestOptions: RequestOptions(path: '/users/$userId')));
|
|
1424
|
+
|
|
1425
|
+
// Act & Assert
|
|
1426
|
+
expect(
|
|
1427
|
+
() => userRepository.getUser(userId),
|
|
1428
|
+
throwsA(isA<UserRepositoryException>()),
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
verify(() => mockDio.get('/users/$userId')).called(1);
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
test('should return users list when getUsers is called', () async {
|
|
1435
|
+
// Arrange
|
|
1436
|
+
final usersJson = [
|
|
1437
|
+
{
|
|
1438
|
+
'id': '1',
|
|
1439
|
+
'name': 'John Doe',
|
|
1440
|
+
'email': 'john@example.com',
|
|
1441
|
+
'avatarUrl': 'https://example.com/avatar1.jpg',
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
'id': '2',
|
|
1445
|
+
'name': 'Jane Smith',
|
|
1446
|
+
'email': 'jane@example.com',
|
|
1447
|
+
'avatarUrl': 'https://example.com/avatar2.jpg',
|
|
1448
|
+
},
|
|
1449
|
+
];
|
|
1450
|
+
|
|
1451
|
+
when(() => mockDio.get('/users', queryParameters: any(named: 'queryParameters')))
|
|
1452
|
+
.thenAnswer((_) async => Response(data: usersJson, statusCode: 200));
|
|
1453
|
+
|
|
1454
|
+
// Act
|
|
1455
|
+
final result = await userRepository.getUsers();
|
|
1456
|
+
|
|
1457
|
+
// Assert
|
|
1458
|
+
expect(result, hasLength(2));
|
|
1459
|
+
expect(result[0].name, equals('John Doe'));
|
|
1460
|
+
expect(result[1].name, equals('Jane Smith'));
|
|
1461
|
+
|
|
1462
|
+
verify(() => mockDio.get('/users', queryParameters: any(named: 'queryParameters'))).called(1);
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
group('User model', () {
|
|
1467
|
+
test('should create user with valid data', () {
|
|
1468
|
+
// Arrange
|
|
1469
|
+
const user = User(
|
|
1470
|
+
id: '123',
|
|
1471
|
+
name: 'John Doe',
|
|
1472
|
+
email: 'john@example.com',
|
|
1473
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
// Act & Assert
|
|
1477
|
+
expect(user.id, equals('123'));
|
|
1478
|
+
expect(user.name, equals('John Doe'));
|
|
1479
|
+
expect(user.email, equals('john@example.com'));
|
|
1480
|
+
expect(user.avatarUrl, equals('https://example.com/avatar.jpg'));
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
test('should copy user with updated name', () {
|
|
1484
|
+
// Arrange
|
|
1485
|
+
const originalUser = User(
|
|
1486
|
+
id: '123',
|
|
1487
|
+
name: 'John Doe',
|
|
1488
|
+
email: 'john@example.com',
|
|
1489
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1490
|
+
);
|
|
1491
|
+
|
|
1492
|
+
// Act
|
|
1493
|
+
final updatedUser = originalUser.copyWith(name: 'Jane Doe');
|
|
1494
|
+
|
|
1495
|
+
// Assert
|
|
1496
|
+
expect(updatedUser.id, equals(originalUser.id));
|
|
1497
|
+
expect(updatedUser.name, equals('Jane Doe'));
|
|
1498
|
+
expect(updatedUser.email, equals(originalUser.email));
|
|
1499
|
+
expect(updatedUser.avatarUrl, equals(originalUser.avatarUrl));
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
test('should compare users correctly', () {
|
|
1503
|
+
// Arrange
|
|
1504
|
+
const user1 = User(
|
|
1505
|
+
id: '123',
|
|
1506
|
+
name: 'John Doe',
|
|
1507
|
+
email: 'john@example.com',
|
|
1508
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
const user2 = User(
|
|
1512
|
+
id: '123',
|
|
1513
|
+
name: 'John Doe',
|
|
1514
|
+
email: 'john@example.com',
|
|
1515
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1516
|
+
);
|
|
1517
|
+
|
|
1518
|
+
const user3 = User(
|
|
1519
|
+
id: '456',
|
|
1520
|
+
name: 'John Doe',
|
|
1521
|
+
email: 'john@example.com',
|
|
1522
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1525
|
+
// Act & Assert
|
|
1526
|
+
expect(user1, equals(user2));
|
|
1527
|
+
expect(user1, isNot(equals(user3)));
|
|
1528
|
+
expect(user1.hashCode, equals(user2.hashCode));
|
|
1529
|
+
expect(user1.hashCode, isNot(equals(user3.hashCode)));
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// Test for extensions
|
|
1535
|
+
void main() {
|
|
1536
|
+
group('StringExtension', () {
|
|
1537
|
+
test('should validate email correctly', () {
|
|
1538
|
+
expect('test@example.com'.isValidEmail, isTrue);
|
|
1539
|
+
expect('test.name@example.com'.isValidEmail, isTrue);
|
|
1540
|
+
expect('test+tag@example.com'.isValidEmail, isTrue);
|
|
1541
|
+
|
|
1542
|
+
expect('invalid-email'.isValidEmail, isFalse);
|
|
1543
|
+
expect('@example.com'.isValidEmail, isFalse);
|
|
1544
|
+
expect('test@'.isValidEmail, isFalse);
|
|
1545
|
+
expect('test@example'.isValidEmail, isFalse);
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
test('should capitalize first letter correctly', () {
|
|
1549
|
+
expect('hello'.capitalize, equals('Hello'));
|
|
1550
|
+
expect('WORLD'.capitalize, equals('WORLD'));
|
|
1551
|
+
expect(''.capitalize, equals(''));
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
test('should truncate string correctly', () {
|
|
1555
|
+
expect('short'.truncate(10), equals('short'));
|
|
1556
|
+
expect('this is a long string'.truncate(10), equals('this is a...'));
|
|
1557
|
+
expect('this is a long string'.truncate(10, suffix: ' [more]'), equals('this is a [more]'));
|
|
1558
|
+
});
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
### Widget Testing
|
|
1564
|
+
|
|
1565
|
+
```dart
|
|
1566
|
+
// Widget tests for UI components
|
|
1567
|
+
void main() {
|
|
1568
|
+
group('UserCard Widget Tests', () {
|
|
1569
|
+
testWidgets('should display user information correctly', (WidgetTester tester) async {
|
|
1570
|
+
// Arrange
|
|
1571
|
+
const user = User(
|
|
1572
|
+
id: '123',
|
|
1573
|
+
name: 'John Doe',
|
|
1574
|
+
email: 'john@example.com',
|
|
1575
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
// Act
|
|
1579
|
+
await tester.pumpWidget(
|
|
1580
|
+
MaterialApp(
|
|
1581
|
+
home: Scaffold(
|
|
1582
|
+
body: UserCard(user: user),
|
|
1583
|
+
),
|
|
1584
|
+
),
|
|
1585
|
+
);
|
|
1586
|
+
|
|
1587
|
+
// Assert
|
|
1588
|
+
expect(find.text('John Doe'), findsOneWidget);
|
|
1589
|
+
expect(find.text('john@example.com'), findsOneWidget);
|
|
1590
|
+
expect(find.byType(CircleAvatar), findsOneWidget);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
testWidgets('should call onTap when card is tapped', (WidgetTester tester) async {
|
|
1594
|
+
// Arrange
|
|
1595
|
+
const user = User(
|
|
1596
|
+
id: '123',
|
|
1597
|
+
name: 'John Doe',
|
|
1598
|
+
email: 'john@example.com',
|
|
1599
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1600
|
+
);
|
|
1601
|
+
|
|
1602
|
+
bool wasTapped = false;
|
|
1603
|
+
|
|
1604
|
+
// Act
|
|
1605
|
+
await tester.pumpWidget(
|
|
1606
|
+
MaterialApp(
|
|
1607
|
+
home: Scaffold(
|
|
1608
|
+
body: UserCard(
|
|
1609
|
+
user: user,
|
|
1610
|
+
onTap: () => wasTapped = true,
|
|
1611
|
+
),
|
|
1612
|
+
),
|
|
1613
|
+
),
|
|
1614
|
+
);
|
|
1615
|
+
|
|
1616
|
+
await tester.tap(find.byType(UserCard));
|
|
1617
|
+
await tester.pump();
|
|
1618
|
+
|
|
1619
|
+
// Assert
|
|
1620
|
+
expect(wasTapped, isTrue);
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
testWidgets('should show edit and delete buttons when callbacks provided', (WidgetTester tester) async {
|
|
1624
|
+
// Arrange
|
|
1625
|
+
const user = User(
|
|
1626
|
+
id: '123',
|
|
1627
|
+
name: 'John Doe',
|
|
1628
|
+
email: 'john@example.com',
|
|
1629
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
// Act
|
|
1633
|
+
await tester.pumpWidget(
|
|
1634
|
+
MaterialApp(
|
|
1635
|
+
home: Scaffold(
|
|
1636
|
+
body: UserCard(
|
|
1637
|
+
user: user,
|
|
1638
|
+
onEdit: () {},
|
|
1639
|
+
onDelete: () {},
|
|
1640
|
+
),
|
|
1641
|
+
),
|
|
1642
|
+
),
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
// Assert
|
|
1646
|
+
expect(find.byIcon(Icons.edit_outlined), findsOneWidget);
|
|
1647
|
+
expect(find.byIcon(Icons.delete_outline), findsOneWidget);
|
|
1648
|
+
});
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
group('UserListScreen Widget Tests', () {
|
|
1652
|
+
testWidgets('should show loading indicator initially', (WidgetTester tester) async {
|
|
1653
|
+
// Arrange
|
|
1654
|
+
await tester.pumpWidget(
|
|
1655
|
+
ProviderScope(
|
|
1656
|
+
overrides: [
|
|
1657
|
+
usersProvider.overrideWith((ref) => AsyncValue.loading()),
|
|
1658
|
+
],
|
|
1659
|
+
child: MaterialApp(
|
|
1660
|
+
home: UserListScreen(),
|
|
1661
|
+
),
|
|
1662
|
+
),
|
|
1663
|
+
);
|
|
1664
|
+
|
|
1665
|
+
// Act & Assert
|
|
1666
|
+
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
testWidgets('should show error message when loading fails', (WidgetTester tester) async {
|
|
1670
|
+
// Arrange
|
|
1671
|
+
const errorMessage = 'Failed to load users';
|
|
1672
|
+
|
|
1673
|
+
await tester.pumpWidget(
|
|
1674
|
+
ProviderScope(
|
|
1675
|
+
overrides: [
|
|
1676
|
+
usersProvider.overrideWith(
|
|
1677
|
+
(ref) => AsyncValue.error(Exception(errorMessage), StackTrace.current),
|
|
1678
|
+
),
|
|
1679
|
+
],
|
|
1680
|
+
child: MaterialApp(
|
|
1681
|
+
home: UserListScreen(),
|
|
1682
|
+
),
|
|
1683
|
+
),
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
await tester.pump();
|
|
1687
|
+
|
|
1688
|
+
// Act & Assert
|
|
1689
|
+
expect(find.text(errorMessage), findsOneWidget);
|
|
1690
|
+
expect(find.byType(ElevatedButton), findsOneWidget);
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
testWidgets('should show users when loading succeeds', (WidgetTester tester) async {
|
|
1694
|
+
// Arrange
|
|
1695
|
+
final users = [
|
|
1696
|
+
const User(
|
|
1697
|
+
id: '1',
|
|
1698
|
+
name: 'John Doe',
|
|
1699
|
+
email: 'john@example.com',
|
|
1700
|
+
avatarUrl: 'https://example.com/avatar1.jpg',
|
|
1701
|
+
),
|
|
1702
|
+
const User(
|
|
1703
|
+
id: '2',
|
|
1704
|
+
name: 'Jane Smith',
|
|
1705
|
+
email: 'jane@example.com',
|
|
1706
|
+
avatarUrl: 'https://example.com/avatar2.jpg',
|
|
1707
|
+
),
|
|
1708
|
+
];
|
|
1709
|
+
|
|
1710
|
+
await tester.pumpWidget(
|
|
1711
|
+
ProviderScope(
|
|
1712
|
+
overrides: [
|
|
1713
|
+
usersProvider.overrideWith((ref) => AsyncValue.data(users)),
|
|
1714
|
+
],
|
|
1715
|
+
child: MaterialApp(
|
|
1716
|
+
home: UserListScreen(),
|
|
1717
|
+
),
|
|
1718
|
+
),
|
|
1719
|
+
);
|
|
1720
|
+
|
|
1721
|
+
await tester.pump();
|
|
1722
|
+
|
|
1723
|
+
// Act & Assert
|
|
1724
|
+
expect(find.text('John Doe'), findsOneWidget);
|
|
1725
|
+
expect(find.text('Jane Smith'), findsOneWidget);
|
|
1726
|
+
expect(find.text('john@example.com'), findsOneWidget);
|
|
1727
|
+
expect(find.text('jane@example.com'), findsOneWidget);
|
|
1728
|
+
expect(find.byType(UserCard), findsNWidgets(2));
|
|
1729
|
+
});
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
### Integration Testing
|
|
1735
|
+
|
|
1736
|
+
```dart
|
|
1737
|
+
// Integration tests with Flutter integration_test package
|
|
1738
|
+
void main() {
|
|
1739
|
+
group('User Management Integration Tests', () {
|
|
1740
|
+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
1741
|
+
|
|
1742
|
+
testWidgets('should complete user creation flow', (WidgetTester tester) async {
|
|
1743
|
+
// Arrange
|
|
1744
|
+
app.main();
|
|
1745
|
+
await tester.pumpAndSettle();
|
|
1746
|
+
|
|
1747
|
+
// Navigate to add user screen
|
|
1748
|
+
await tester.tap(find.byIcon(Icons.add));
|
|
1749
|
+
await tester.pumpAndSettle();
|
|
1750
|
+
|
|
1751
|
+
// Fill out user form
|
|
1752
|
+
await tester.enterText(find.byKey(const Key('name_field')), 'John Doe');
|
|
1753
|
+
await tester.enterText(find.byKey(const Key('email_field')), 'john@example.com');
|
|
1754
|
+
|
|
1755
|
+
// Submit form
|
|
1756
|
+
await tester.tap(find.byKey(const Key('submit_button')));
|
|
1757
|
+
await tester.pumpAndSettle();
|
|
1758
|
+
|
|
1759
|
+
// Assert user appears in list
|
|
1760
|
+
expect(find.text('John Doe'), findsOneWidget);
|
|
1761
|
+
expect(find.text('john@example.com'), findsOneWidget);
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
testWidgets('should navigate to user details and back', (WidgetTester tester) async {
|
|
1765
|
+
// Arrange
|
|
1766
|
+
app.main();
|
|
1767
|
+
await tester.pumpAndSettle();
|
|
1768
|
+
|
|
1769
|
+
// Wait for users to load
|
|
1770
|
+
await tester.pumpAndSettle(const Duration(seconds: 3));
|
|
1771
|
+
|
|
1772
|
+
// Tap on first user
|
|
1773
|
+
await tester.tap(find.byType(UserCard).first);
|
|
1774
|
+
await tester.pumpAndSettle();
|
|
1775
|
+
|
|
1776
|
+
// Assert we're on user details screen
|
|
1777
|
+
expect(find.byType(UserDetailScreen), findsOneWidget);
|
|
1778
|
+
|
|
1779
|
+
// Navigate back
|
|
1780
|
+
await tester.tap(find.byIcon(Icons.arrow_back));
|
|
1781
|
+
await tester.pumpAndSettle();
|
|
1782
|
+
|
|
1783
|
+
// Assert we're back on user list
|
|
1784
|
+
expect(find.byType(UserListScreen), findsOneWidget);
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
testWidgets('should handle network errors gracefully', (WidgetTester tester) async {
|
|
1788
|
+
// Arrange - mock network failure
|
|
1789
|
+
setUpAll(() {
|
|
1790
|
+
HttpOverrides.global = MockHttpOverrides();
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
app.main();
|
|
1794
|
+
await tester.pumpAndSettle();
|
|
1795
|
+
|
|
1796
|
+
// Wait for error to appear
|
|
1797
|
+
await tester.pumpAndSettle(const Duration(seconds: 3));
|
|
1798
|
+
|
|
1799
|
+
// Assert error message is shown
|
|
1800
|
+
expect(find.byType(ErrorWidget), findsOneWidget);
|
|
1801
|
+
|
|
1802
|
+
// Tap retry button
|
|
1803
|
+
await tester.tap(find.byType(ElevatedButton));
|
|
1804
|
+
await tester.pumpAndSettle();
|
|
1805
|
+
|
|
1806
|
+
// Verify retry attempts
|
|
1807
|
+
expect(find.text('Retrying...'), findsOneWidget);
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Mock HTTP overrides for testing
|
|
1813
|
+
class MockHttpOverrides extends HttpOverrides {
|
|
1814
|
+
@override
|
|
1815
|
+
HttpClient createHttpClient(SecurityContext? context) {
|
|
1816
|
+
return MockHttpClient();
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
class MockHttpClient extends FakeHttpClient {
|
|
1821
|
+
@override
|
|
1822
|
+
Future<HttpClientRequest> getUrl(Uri url) async {
|
|
1823
|
+
if (url.path.contains('/users')) {
|
|
1824
|
+
throw HttpClientException('Connection failed');
|
|
1825
|
+
}
|
|
1826
|
+
return super.getUrl(url);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Golden tests for visual regression
|
|
1831
|
+
void main() {
|
|
1832
|
+
group('UserCard Golden Tests', () {
|
|
1833
|
+
testWidgets('should match golden snapshot', (WidgetTester tester) async {
|
|
1834
|
+
// Arrange
|
|
1835
|
+
const user = User(
|
|
1836
|
+
id: '123',
|
|
1837
|
+
name: 'John Doe',
|
|
1838
|
+
email: 'john@example.com',
|
|
1839
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1840
|
+
);
|
|
1841
|
+
|
|
1842
|
+
// Act
|
|
1843
|
+
await tester.pumpWidget(
|
|
1844
|
+
MaterialApp(
|
|
1845
|
+
theme: ThemeData.light(),
|
|
1846
|
+
home: Scaffold(
|
|
1847
|
+
body: UserCard(user: user),
|
|
1848
|
+
),
|
|
1849
|
+
),
|
|
1850
|
+
);
|
|
1851
|
+
|
|
1852
|
+
await tester.pumpAndSettle();
|
|
1853
|
+
|
|
1854
|
+
// Assert
|
|
1855
|
+
await expectLater(
|
|
1856
|
+
find.byType(UserCard),
|
|
1857
|
+
matchesGoldenFile('goldens/user_card.png'),
|
|
1858
|
+
);
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
testWidgets('should match dark mode golden snapshot', (WidgetTester tester) async {
|
|
1862
|
+
// Arrange
|
|
1863
|
+
const user = User(
|
|
1864
|
+
id: '123',
|
|
1865
|
+
name: 'John Doe',
|
|
1866
|
+
email: 'john@example.com',
|
|
1867
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
1868
|
+
);
|
|
1869
|
+
|
|
1870
|
+
// Act
|
|
1871
|
+
await tester.pumpWidget(
|
|
1872
|
+
MaterialApp(
|
|
1873
|
+
theme: ThemeData.dark(),
|
|
1874
|
+
home: Scaffold(
|
|
1875
|
+
body: UserCard(user: user),
|
|
1876
|
+
),
|
|
1877
|
+
),
|
|
1878
|
+
);
|
|
1879
|
+
|
|
1880
|
+
await tester.pumpAndSettle();
|
|
1881
|
+
|
|
1882
|
+
// Assert
|
|
1883
|
+
await expectLater(
|
|
1884
|
+
find.byType(UserCard),
|
|
1885
|
+
matchesGoldenFile('goldens/user_card_dark.png'),
|
|
1886
|
+
);
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
## Security Best Practices
|
|
1893
|
+
|
|
1894
|
+
### Input Validation
|
|
1895
|
+
|
|
1896
|
+
```dart
|
|
1897
|
+
// Input validation utilities
|
|
1898
|
+
class InputValidator {
|
|
1899
|
+
static String? validateEmail(String? value) {
|
|
1900
|
+
if (value == null || value.isEmpty) {
|
|
1901
|
+
return 'Email is required';
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
if (!value.isValidEmail) {
|
|
1905
|
+
return 'Please enter a valid email address';
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
static String? validatePassword(String? value) {
|
|
1912
|
+
if (value == null || value.isEmpty) {
|
|
1913
|
+
return 'Password is required';
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if (value.length < 8) {
|
|
1917
|
+
return 'Password must be at least 8 characters long';
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (!value.contains(RegExp(r'[A-Z]'))) {
|
|
1921
|
+
return 'Password must contain at least one uppercase letter';
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if (!value.contains(RegExp(r'[a-z]'))) {
|
|
1925
|
+
return 'Password must contain at least one lowercase letter';
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (!value.contains(RegExp(r'[0-9]'))) {
|
|
1929
|
+
return 'Password must contain at least one digit';
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
static String? validateName(String? value) {
|
|
1936
|
+
if (value == null || value.isEmpty) {
|
|
1937
|
+
return 'Name is required';
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (value.length < 2) {
|
|
1941
|
+
return 'Name must be at least 2 characters long';
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
if (value.length > 50) {
|
|
1945
|
+
return 'Name must be less than 50 characters';
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (!value.contains(RegExp(r'^[a-zA-Z\s]+$'))) {
|
|
1949
|
+
return 'Name can only contain letters and spaces';
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
return null;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Secure storage for sensitive data
|
|
1957
|
+
class SecureStorage {
|
|
1958
|
+
static const _storage = FlutterSecureStorage();
|
|
1959
|
+
|
|
1960
|
+
static Future<void> storeToken(String token) async {
|
|
1961
|
+
await _storage.write(key: 'auth_token', value: token);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
static Future<String?> getToken() async {
|
|
1965
|
+
return await _storage.read(key: 'auth_token');
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
static Future<void> deleteToken() async {
|
|
1969
|
+
await _storage.delete(key: 'auth_token');
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
static Future<void> storeUserCredentials(String username, String password) async {
|
|
1973
|
+
await _storage.write(key: 'username', value: username);
|
|
1974
|
+
await _storage.write(key: 'password', value: password);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
static Future<Map<String, String?>> getUserCredentials() async {
|
|
1978
|
+
final username = await _storage.read(key: 'username');
|
|
1979
|
+
final password = await _storage.read(key: 'password');
|
|
1980
|
+
return {'username': username, 'password': password};
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
static Future<void> clearAll() async {
|
|
1984
|
+
await _storage.deleteAll();
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Secure HTTP client with authentication
|
|
1989
|
+
class SecureHttpClient {
|
|
1990
|
+
late final Dio _dio;
|
|
1991
|
+
|
|
1992
|
+
SecureHttpClient() {
|
|
1993
|
+
_dio = Dio(BaseOptions(
|
|
1994
|
+
baseUrl: AppConfig.apiBaseUrl,
|
|
1995
|
+
connectTimeout: const Duration(seconds: 30),
|
|
1996
|
+
receiveTimeout: const Duration(seconds: 30),
|
|
1997
|
+
));
|
|
1998
|
+
|
|
1999
|
+
_setupInterceptors();
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
void _setupInterceptors() {
|
|
2003
|
+
// Authentication interceptor
|
|
2004
|
+
_dio.interceptors.add(
|
|
2005
|
+
InterceptorsWrapper(
|
|
2006
|
+
onRequest: (options, handler) async {
|
|
2007
|
+
final token = await SecureStorage.getToken();
|
|
2008
|
+
if (token != null) {
|
|
2009
|
+
options.headers['Authorization'] = 'Bearer $token';
|
|
2010
|
+
}
|
|
2011
|
+
handler.next(options);
|
|
2012
|
+
},
|
|
2013
|
+
onError: (error, handler) async {
|
|
2014
|
+
if (error.response?.statusCode == 401) {
|
|
2015
|
+
// Token expired, clear storage and navigate to login
|
|
2016
|
+
await SecureStorage.deleteToken();
|
|
2017
|
+
// Navigate to login screen
|
|
2018
|
+
_navigateToLogin();
|
|
2019
|
+
}
|
|
2020
|
+
handler.next(error);
|
|
2021
|
+
},
|
|
2022
|
+
),
|
|
2023
|
+
);
|
|
2024
|
+
|
|
2025
|
+
// Logging interceptor (only in debug mode)
|
|
2026
|
+
if (AppConfig.isDebug) {
|
|
2027
|
+
_dio.interceptors.add(LogInterceptor(
|
|
2028
|
+
requestBody: true,
|
|
2029
|
+
responseBody: true,
|
|
2030
|
+
));
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// Retry interceptor
|
|
2034
|
+
_dio.interceptors.add(RetryInterceptor(
|
|
2035
|
+
dio: _dio,
|
|
2036
|
+
options: const RetryOptions(
|
|
2037
|
+
retries: 3,
|
|
2038
|
+
retryInterval: Duration(seconds: 1),
|
|
2039
|
+
),
|
|
2040
|
+
));
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
void _navigateToLogin() {
|
|
2044
|
+
// Use navigator key to navigate to login screen
|
|
2045
|
+
navigatorKey.currentState?.pushNamedAndRemoveUntil(
|
|
2046
|
+
'/login',
|
|
2047
|
+
(route) => false,
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
Future<Response<T>> get<T>(
|
|
2052
|
+
String path, {
|
|
2053
|
+
Map<String, dynamic>? queryParameters,
|
|
2054
|
+
Options? options,
|
|
2055
|
+
}) async {
|
|
2056
|
+
return _dio.get<T>(path, queryParameters: queryParameters, options: options);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
Future<Response<T>> post<T>(
|
|
2060
|
+
String path, {
|
|
2061
|
+
dynamic data,
|
|
2062
|
+
Map<String, dynamic>? queryParameters,
|
|
2063
|
+
Options? options,
|
|
2064
|
+
}) async {
|
|
2065
|
+
return _dio.post<T>(
|
|
2066
|
+
path,
|
|
2067
|
+
data: data,
|
|
2068
|
+
queryParameters: queryParameters,
|
|
2069
|
+
options: options,
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
Future<Response<T>> put<T>(
|
|
2074
|
+
String path, {
|
|
2075
|
+
dynamic data,
|
|
2076
|
+
Map<String, dynamic>? queryParameters,
|
|
2077
|
+
Options? options,
|
|
2078
|
+
}) async {
|
|
2079
|
+
return _dio.put<T>(
|
|
2080
|
+
path,
|
|
2081
|
+
data: data,
|
|
2082
|
+
queryParameters: queryParameters,
|
|
2083
|
+
options: options,
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
Future<Response<T>> delete<T>(
|
|
2088
|
+
String path, {
|
|
2089
|
+
dynamic data,
|
|
2090
|
+
Map<String, dynamic>? queryParameters,
|
|
2091
|
+
Options? options,
|
|
2092
|
+
}) async {
|
|
2093
|
+
return _dio.delete<T>(
|
|
2094
|
+
path,
|
|
2095
|
+
data: data,
|
|
2096
|
+
queryParameters: queryParameters,
|
|
2097
|
+
options: options,
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Local authentication with biometrics
|
|
2103
|
+
class BiometricAuth {
|
|
2104
|
+
static final LocalAuthentication _auth = LocalAuthentication();
|
|
2105
|
+
|
|
2106
|
+
static Future<bool> isAvailable() async {
|
|
2107
|
+
final isAvailable = await _auth.canCheckBiometrics;
|
|
2108
|
+
final isDeviceSupported = await _auth.isDeviceSupported();
|
|
2109
|
+
return isAvailable && isDeviceSupported;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
static Future<bool> authenticate() async {
|
|
2113
|
+
try {
|
|
2114
|
+
final didAuthenticate = await _auth.authenticate(
|
|
2115
|
+
localizedReason: 'Please authenticate to access this feature',
|
|
2116
|
+
options: const AuthenticationOptions(
|
|
2117
|
+
biometricOnly: false,
|
|
2118
|
+
useErrorDialogs: true,
|
|
2119
|
+
stickyAuth: true,
|
|
2120
|
+
),
|
|
2121
|
+
);
|
|
2122
|
+
return didAuthenticate;
|
|
2123
|
+
} catch (e) {
|
|
2124
|
+
return false;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
static Future<List<BiometricType>> getAvailableBiometrics() async {
|
|
2129
|
+
try {
|
|
2130
|
+
return await _auth.getAvailableBiometrics();
|
|
2131
|
+
} catch (e) {
|
|
2132
|
+
return [];
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
```
|
|
2137
|
+
|
|
2138
|
+
### Data Protection
|
|
2139
|
+
|
|
2140
|
+
```dart
|
|
2141
|
+
// Encrypted data model
|
|
2142
|
+
class SecureUser {
|
|
2143
|
+
final String id;
|
|
2144
|
+
final String encryptedName;
|
|
2145
|
+
final String encryptedEmail;
|
|
2146
|
+
|
|
2147
|
+
SecureUser({
|
|
2148
|
+
required this.id,
|
|
2149
|
+
required this.encryptedName,
|
|
2150
|
+
required this.encryptedEmail,
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
factory SecureUser.fromUser(User user, EncryptionService encryptionService) {
|
|
2154
|
+
return SecureUser(
|
|
2155
|
+
id: user.id,
|
|
2156
|
+
encryptedName: encryptionService.encrypt(user.name),
|
|
2157
|
+
encryptedEmail: encryptionService.encrypt(user.email),
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
User toUser(EncryptionService encryptionService) {
|
|
2162
|
+
return User(
|
|
2163
|
+
id: id,
|
|
2164
|
+
name: encryptionService.decrypt(encryptedName),
|
|
2165
|
+
email: encryptionService.decrypt(encryptedEmail),
|
|
2166
|
+
avatarUrl: '', // Not encrypted in this example
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// Encryption service
|
|
2172
|
+
class EncryptionService {
|
|
2173
|
+
static final EncryptionService _instance = EncryptionService._internal();
|
|
2174
|
+
factory EncryptionService() => _instance;
|
|
2175
|
+
EncryptionService._internal();
|
|
2176
|
+
|
|
2177
|
+
late final Encrypter _encrypter;
|
|
2178
|
+
late final IV _iv;
|
|
2179
|
+
|
|
2180
|
+
Future<void> initialize() async {
|
|
2181
|
+
final key = await _getOrCreateKey();
|
|
2182
|
+
_encrypter = Encrypter(AES(key));
|
|
2183
|
+
_iv = IV.fromLength(16);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
Future<Key> _getOrCreateKey() async {
|
|
2187
|
+
const storage = FlutterSecureStorage();
|
|
2188
|
+
|
|
2189
|
+
String? keyString = await storage.read(key: 'encryption_key');
|
|
2190
|
+
if (keyString != null) {
|
|
2191
|
+
return Key.fromBase64(keyString);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
final key = Key.fromSecureRandom(32);
|
|
2195
|
+
await storage.write(key: 'encryption_key', value: key.base64);
|
|
2196
|
+
return key;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
String encrypt(String plaintext) {
|
|
2200
|
+
final encrypted = _encrypter.encrypt(plaintext, iv: _iv);
|
|
2201
|
+
return encrypted.base64;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
String decrypt(String ciphertext) {
|
|
2205
|
+
final encrypted = Encrypted.fromBase64(ciphertext);
|
|
2206
|
+
return _encrypter.decrypt(encrypted, iv: _iv);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
Future<void> clearKey() async {
|
|
2210
|
+
const storage = FlutterSecureStorage();
|
|
2211
|
+
await storage.delete(key: 'encryption_key');
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// API security with rate limiting
|
|
2216
|
+
class RateLimiter {
|
|
2217
|
+
final Map<String, List<DateTime>> _requests = {};
|
|
2218
|
+
final int maxRequests;
|
|
2219
|
+
final Duration timeWindow;
|
|
2220
|
+
|
|
2221
|
+
RateLimiter({
|
|
2222
|
+
this.maxRequests = 100,
|
|
2223
|
+
this.timeWindow = const Duration(minutes: 1),
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
bool canMakeRequest(String identifier) {
|
|
2227
|
+
final now = DateTime.now();
|
|
2228
|
+
final requests = _requests[identifier] ?? [];
|
|
2229
|
+
|
|
2230
|
+
// Remove old requests outside the time window
|
|
2231
|
+
requests.removeWhere((request) => now.difference(request) > timeWindow);
|
|
2232
|
+
|
|
2233
|
+
if (requests.length >= maxRequests) {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
requests.add(now);
|
|
2238
|
+
_requests[identifier] = requests;
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
Duration? getTimeUntilNextRequest(String identifier) {
|
|
2243
|
+
final now = DateTime.now();
|
|
2244
|
+
final requests = _requests[identifier] ?? [];
|
|
2245
|
+
|
|
2246
|
+
if (requests.length < maxRequests) {
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
final oldestRequest = requests.first;
|
|
2251
|
+
final timeSinceOldest = now.difference(oldestRequest);
|
|
2252
|
+
|
|
2253
|
+
if (timeSinceOldest > timeWindow) {
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
return timeWindow - timeSinceOldest;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Content security for web applications
|
|
2262
|
+
class ContentSecurityPolicy {
|
|
2263
|
+
static String get policy {
|
|
2264
|
+
return [
|
|
2265
|
+
"default-src 'self'",
|
|
2266
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
2267
|
+
"style-src 'self' 'unsafe-inline'",
|
|
2268
|
+
"img-src 'self' data: https:",
|
|
2269
|
+
"font-src 'self' data:",
|
|
2270
|
+
"connect-src 'self' https://api.example.com",
|
|
2271
|
+
"media-src 'self'",
|
|
2272
|
+
"object-src 'none'",
|
|
2273
|
+
"base-uri 'self'",
|
|
2274
|
+
"form-action 'self'",
|
|
2275
|
+
"frame-ancestors 'none'",
|
|
2276
|
+
"upgrade-insecure-requests",
|
|
2277
|
+
].join('; ');
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
64
2280
|
```
|
|
65
2281
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- Relevant test suites and sample data.
|
|
2282
|
+
## Integration Patterns
|
|
2283
|
+
|
|
2284
|
+
### Server-Side Dart with Shelf
|
|
70
2285
|
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
2286
|
+
```dart
|
|
2287
|
+
// Server-side Dart application
|
|
2288
|
+
import 'dart:io';
|
|
2289
|
+
import 'dart:convert';
|
|
2290
|
+
import 'package:shelf/shelf.dart';
|
|
2291
|
+
import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
2292
|
+
import 'package:shelf_router/shelf_router.dart';
|
|
2293
|
+
import 'package:shelf_cors_headers/shelf_cors_headers.dart';
|
|
2294
|
+
import 'package:shelf_static/shelf_static.dart';
|
|
74
2295
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
2296
|
+
// User service for server-side
|
|
2297
|
+
class UserService {
|
|
2298
|
+
final Map<String, User> _users = {};
|
|
2299
|
+
|
|
2300
|
+
UserService() {
|
|
2301
|
+
_seedUsers();
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
void _seedUsers() {
|
|
2305
|
+
_users['1'] = const User(
|
|
2306
|
+
id: '1',
|
|
2307
|
+
name: 'John Doe',
|
|
2308
|
+
email: 'john@example.com',
|
|
2309
|
+
avatarUrl: 'https://example.com/avatar1.jpg',
|
|
2310
|
+
);
|
|
2311
|
+
_users['2'] = const User(
|
|
2312
|
+
id: '2',
|
|
2313
|
+
name: 'Jane Smith',
|
|
2314
|
+
email: 'jane@example.com',
|
|
2315
|
+
avatarUrl: 'https://example.com/avatar2.jpg',
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
List<User> getUsers({int page = 1, int limit = 20}) {
|
|
2320
|
+
final skip = (page - 1) * limit;
|
|
2321
|
+
return _users.values.skip(skip).take(limit).toList();
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
User? getUser(String id) {
|
|
2325
|
+
return _users[id];
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
User createUser(CreateUserRequest request) {
|
|
2329
|
+
final id = (_users.keys.length + 1).toString();
|
|
2330
|
+
final user = User(
|
|
2331
|
+
id: id,
|
|
2332
|
+
name: request.name,
|
|
2333
|
+
email: request.email,
|
|
2334
|
+
avatarUrl: request.avatarUrl ?? '',
|
|
2335
|
+
);
|
|
2336
|
+
_users[id] = user;
|
|
2337
|
+
return user;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
User? updateUser(String id, UpdateUserRequest request) {
|
|
2341
|
+
final user = _users[id];
|
|
2342
|
+
if (user == null) return null;
|
|
2343
|
+
|
|
2344
|
+
final updatedUser = user.copyWith(
|
|
2345
|
+
name: request.name ?? user.name,
|
|
2346
|
+
email: request.email ?? user.email,
|
|
2347
|
+
avatarUrl: request.avatarUrl ?? user.avatarUrl,
|
|
2348
|
+
);
|
|
2349
|
+
|
|
2350
|
+
_users[id] = updatedUser;
|
|
2351
|
+
return updatedUser;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
bool deleteUser(String id) {
|
|
2355
|
+
return _users.remove(id) != null;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
78
2358
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
2359
|
+
// Request handlers
|
|
2360
|
+
Handler getUsersHandler(UserService userService) {
|
|
2361
|
+
return (Request request) async {
|
|
2362
|
+
final page = int.tryParse(request.url.queryParameters['page'] ?? '1') ?? 1;
|
|
2363
|
+
final limit = int.tryParse(request.url.queryParameters['limit'] ?? '20') ?? 20;
|
|
2364
|
+
|
|
2365
|
+
final users = userService.getUsers(page: page, limit: limit);
|
|
2366
|
+
|
|
2367
|
+
final response = {
|
|
2368
|
+
'users': users.map((u) => u.toJson()).toList(),
|
|
2369
|
+
'page': page,
|
|
2370
|
+
'limit': limit,
|
|
2371
|
+
'total': users.length,
|
|
2372
|
+
};
|
|
2373
|
+
|
|
2374
|
+
return Response.ok(
|
|
2375
|
+
json.encode(response),
|
|
2376
|
+
headers: {'content-type': 'application/json'},
|
|
2377
|
+
);
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
82
2380
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
2381
|
+
Handler getUserHandler(UserService userService) {
|
|
2382
|
+
return (Request request) {
|
|
2383
|
+
final id = request.params['id']!;
|
|
2384
|
+
final user = userService.getUser(id);
|
|
2385
|
+
|
|
2386
|
+
if (user == null) {
|
|
2387
|
+
return Response.notFound(json.encode({'error': 'User not found'}));
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
return Response.ok(
|
|
2391
|
+
json.encode(user.toJson()),
|
|
2392
|
+
headers: {'content-type': 'application/json'},
|
|
2393
|
+
);
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
86
2396
|
|
|
87
|
-
|
|
88
|
-
|
|
2397
|
+
Handler createUserHandler(UserService userService) {
|
|
2398
|
+
return (Request request) async {
|
|
2399
|
+
try {
|
|
2400
|
+
final body = await request.readAsString();
|
|
2401
|
+
final jsonData = json.decode(body) as Map<String, dynamic>;
|
|
2402
|
+
final createRequest = CreateUserRequest.fromJson(jsonData);
|
|
2403
|
+
|
|
2404
|
+
final user = userService.createUser(createRequest);
|
|
2405
|
+
|
|
2406
|
+
return Response(201,
|
|
2407
|
+
body: json.encode(user.toJson()),
|
|
2408
|
+
headers: {'content-type': 'application/json'},
|
|
2409
|
+
);
|
|
2410
|
+
} catch (e) {
|
|
2411
|
+
return Response(400,
|
|
2412
|
+
body: json.encode({'error': 'Invalid request data: $e'}),
|
|
2413
|
+
headers: {'content-type': 'application/json'},
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
89
2418
|
|
|
90
|
-
|
|
2419
|
+
// Router setup
|
|
2420
|
+
Router setupRouter(UserService userService) {
|
|
2421
|
+
final router = Router();
|
|
2422
|
+
|
|
2423
|
+
// CORS headers
|
|
2424
|
+
router.all('/<ignored|.*>', (Request request) {
|
|
2425
|
+
return Response.ok(null);
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
// API routes
|
|
2429
|
+
router.get('/users', getUsersHandler(userService));
|
|
2430
|
+
router.get('/users/<id>', getUserHandler(userService));
|
|
2431
|
+
router.post('/users', createUserHandler(userService));
|
|
2432
|
+
router.put('/users/<id>', updateUserHandler(userService));
|
|
2433
|
+
router.delete('/users/<id>', deleteUserHandler(userService));
|
|
2434
|
+
|
|
2435
|
+
// Static file serving
|
|
2436
|
+
router.get('/<.*>', (Request request) {
|
|
2437
|
+
final path = request.url.path;
|
|
2438
|
+
if (path.startsWith('/api/')) {
|
|
2439
|
+
return Response.notFound('Not found');
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
return staticHandler('web')(request);
|
|
2443
|
+
});
|
|
2444
|
+
|
|
2445
|
+
return router;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// Static file handler
|
|
2449
|
+
Handler staticHandler(String directory) {
|
|
2450
|
+
return createStaticHandler(directory, defaultDocument: 'index.html');
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// Main server function
|
|
2454
|
+
Future<void> main() async {
|
|
2455
|
+
final userService = UserService();
|
|
2456
|
+
final router = setupRouter(userService);
|
|
2457
|
+
|
|
2458
|
+
// Add CORS middleware
|
|
2459
|
+
final handler = const Pipeline()
|
|
2460
|
+
.addMiddleware(corsHeaders())
|
|
2461
|
+
.addMiddleware(logRequests())
|
|
2462
|
+
.addHandler(router);
|
|
2463
|
+
|
|
2464
|
+
// Start server
|
|
2465
|
+
final server = await shelf_io.serve(
|
|
2466
|
+
handler,
|
|
2467
|
+
InternetAddress.anyIPv4,
|
|
2468
|
+
8080,
|
|
2469
|
+
);
|
|
2470
|
+
|
|
2471
|
+
print('Server listening on port ${server.port}');
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// Logging middleware
|
|
2475
|
+
Middleware logRequests() {
|
|
2476
|
+
return (Handler innerHandler) {
|
|
2477
|
+
return (Request request) async {
|
|
2478
|
+
final startTime = DateTime.now();
|
|
2479
|
+
|
|
2480
|
+
try {
|
|
2481
|
+
final response = await innerHandler(request);
|
|
2482
|
+
final duration = DateTime.now().difference(startTime);
|
|
2483
|
+
|
|
2484
|
+
print(
|
|
2485
|
+
'${request.method} ${request.requestedUri} -> '
|
|
2486
|
+
'${response.statusCode} (${duration.inMilliseconds}ms)',
|
|
2487
|
+
);
|
|
2488
|
+
|
|
2489
|
+
return response;
|
|
2490
|
+
} catch (error, stackTrace) {
|
|
2491
|
+
final duration = DateTime.now().difference(startTime);
|
|
2492
|
+
|
|
2493
|
+
print(
|
|
2494
|
+
'${request.method} ${request.requestedUri} -> ERROR ($duration): $error',
|
|
2495
|
+
);
|
|
2496
|
+
|
|
2497
|
+
rethrow;
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
```
|
|
2503
|
+
|
|
2504
|
+
### WebSockets for Real-time Communication
|
|
2505
|
+
|
|
2506
|
+
```dart
|
|
2507
|
+
// WebSocket server implementation
|
|
2508
|
+
import 'dart:io';
|
|
2509
|
+
import 'dart:convert';
|
|
2510
|
+
|
|
2511
|
+
class WebSocketServer {
|
|
2512
|
+
late HttpServer _server;
|
|
2513
|
+
final Set<WebSocket> _connections = {};
|
|
2514
|
+
|
|
2515
|
+
Future<void> start(int port) async {
|
|
2516
|
+
_server = await HttpServer.bind(InternetAddress.anyIPv4, port);
|
|
2517
|
+
print('WebSocket server listening on port $port');
|
|
2518
|
+
|
|
2519
|
+
await for (HttpRequest request in _server) {
|
|
2520
|
+
if (request.uri.path == '/ws') {
|
|
2521
|
+
await _handleWebSocket(request);
|
|
2522
|
+
} else {
|
|
2523
|
+
request.response.statusCode = HttpStatus.notFound;
|
|
2524
|
+
await request.response.close();
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
Future<void> _handleWebSocket(HttpRequest request) async {
|
|
2530
|
+
try {
|
|
2531
|
+
final webSocket = await WebSocketTransformer.upgrade(request);
|
|
2532
|
+
_connections.add(webSocket);
|
|
2533
|
+
|
|
2534
|
+
print('New WebSocket connection: ${webSocket.hashCode}');
|
|
2535
|
+
|
|
2536
|
+
// Send welcome message
|
|
2537
|
+
_sendMessage(webSocket, {
|
|
2538
|
+
'type': 'welcome',
|
|
2539
|
+
'message': 'Connected to chat server',
|
|
2540
|
+
'timestamp': DateTime.now().toIso8601String(),
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2543
|
+
// Listen for messages
|
|
2544
|
+
webSocket.listen(
|
|
2545
|
+
(data) => _handleMessage(webSocket, data),
|
|
2546
|
+
onDone: () => _handleDisconnection(webSocket),
|
|
2547
|
+
onError: (error) => print('WebSocket error: $error'),
|
|
2548
|
+
);
|
|
2549
|
+
} catch (e) {
|
|
2550
|
+
print('Failed to upgrade to WebSocket: $e');
|
|
2551
|
+
request.response.statusCode = HttpStatus.internalServerError;
|
|
2552
|
+
await request.response.close();
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
void _handleMessage(WebSocket webSocket, dynamic data) {
|
|
2557
|
+
try {
|
|
2558
|
+
final message = json.decode(data as String) as Map<String, dynamic>;
|
|
2559
|
+
final messageWithTimestamp = {
|
|
2560
|
+
...message,
|
|
2561
|
+
'timestamp': DateTime.now().toIso8601String(),
|
|
2562
|
+
};
|
|
2563
|
+
|
|
2564
|
+
print('Received message: $messageWithTimestamp');
|
|
2565
|
+
|
|
2566
|
+
// Broadcast message to all connected clients
|
|
2567
|
+
_broadcastMessage(messageWithTimestamp);
|
|
2568
|
+
} catch (e) {
|
|
2569
|
+
print('Error handling message: $e');
|
|
2570
|
+
_sendMessage(webSocket, {
|
|
2571
|
+
'type': 'error',
|
|
2572
|
+
'message': 'Invalid message format',
|
|
2573
|
+
'timestamp': DateTime.now().toIso8601String(),
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
void _handleDisconnection(WebSocket webSocket) {
|
|
2579
|
+
_connections.remove(webSocket);
|
|
2580
|
+
print('WebSocket disconnected: ${webSocket.hashCode}');
|
|
2581
|
+
|
|
2582
|
+
_broadcastMessage({
|
|
2583
|
+
'type': 'user_disconnected',
|
|
2584
|
+
'message': 'A user left the chat',
|
|
2585
|
+
'timestamp': DateTime.now().toIso8601String(),
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
void _sendMessage(WebSocket webSocket, Map<String, dynamic> message) {
|
|
2590
|
+
try {
|
|
2591
|
+
webSocket.add(json.encode(message));
|
|
2592
|
+
} catch (e) {
|
|
2593
|
+
print('Error sending message: $e');
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
void _broadcastMessage(Map<String, dynamic> message) {
|
|
2598
|
+
final messageString = json.encode(message);
|
|
2599
|
+
|
|
2600
|
+
for (final connection in _connections) {
|
|
2601
|
+
try {
|
|
2602
|
+
connection.add(messageString);
|
|
2603
|
+
} catch (e) {
|
|
2604
|
+
print('Error broadcasting message: $e');
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
Future<void> stop() async {
|
|
2610
|
+
await _server.close();
|
|
2611
|
+
for (final connection in _connections) {
|
|
2612
|
+
await connection.close();
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// Chat message models
|
|
2618
|
+
class ChatMessage {
|
|
2619
|
+
final String id;
|
|
2620
|
+
final String username;
|
|
2621
|
+
final String content;
|
|
2622
|
+
final DateTime timestamp;
|
|
2623
|
+
|
|
2624
|
+
ChatMessage({
|
|
2625
|
+
required this.id,
|
|
2626
|
+
required this.username,
|
|
2627
|
+
required this.content,
|
|
2628
|
+
required this.timestamp,
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
Map<String, dynamic> toJson() {
|
|
2632
|
+
return {
|
|
2633
|
+
'id': id,
|
|
2634
|
+
'username': username,
|
|
2635
|
+
'content': content,
|
|
2636
|
+
'timestamp': timestamp.toIso8601String(),
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
factory ChatMessage.fromJson(Map<String, dynamic> json) {
|
|
2641
|
+
return ChatMessage(
|
|
2642
|
+
id: json['id'] as String,
|
|
2643
|
+
username: json['username'] as String,
|
|
2644
|
+
content: json['content'] as String,
|
|
2645
|
+
timestamp: DateTime.parse(json['timestamp'] as String),
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// WebSocket client for Flutter
|
|
2651
|
+
class WebSocketService {
|
|
2652
|
+
WebSocketChannel? _channel;
|
|
2653
|
+
final StreamController<ChatMessage> _messageController = StreamController<ChatMessage>.broadcast();
|
|
2654
|
+
final StreamController<String> _statusController = StreamController<String>.broadcast();
|
|
2655
|
+
|
|
2656
|
+
Stream<ChatMessage> get messageStream => _messageController.stream;
|
|
2657
|
+
Stream<String> get statusStream => _statusController.stream;
|
|
2658
|
+
|
|
2659
|
+
Future<void> connect(String url) async {
|
|
2660
|
+
try {
|
|
2661
|
+
_channel = WebSocketChannel.connect(Uri.parse(url));
|
|
2662
|
+
_statusController.add('Connected');
|
|
2663
|
+
|
|
2664
|
+
_channel!.stream.listen(
|
|
2665
|
+
(data) {
|
|
2666
|
+
try {
|
|
2667
|
+
final message = ChatMessage.fromJson(json.decode(data));
|
|
2668
|
+
_messageController.add(message);
|
|
2669
|
+
} catch (e) {
|
|
2670
|
+
print('Error parsing message: $e');
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2673
|
+
onDone: () {
|
|
2674
|
+
_statusController.add('Disconnected');
|
|
2675
|
+
},
|
|
2676
|
+
onError: (error) {
|
|
2677
|
+
_statusController.add('Error: $error');
|
|
2678
|
+
},
|
|
2679
|
+
);
|
|
2680
|
+
} catch (e) {
|
|
2681
|
+
_statusController.add('Connection failed: $e');
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
void sendMessage(ChatMessage message) {
|
|
2686
|
+
if (_channel != null) {
|
|
2687
|
+
_channel!.sink.add(json.encode(message.toJson()));
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
void disconnect() {
|
|
2692
|
+
_channel?.sink.close();
|
|
2693
|
+
_channel = null;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
void dispose() {
|
|
2697
|
+
disconnect();
|
|
2698
|
+
_messageController.close();
|
|
2699
|
+
_statusController.close();
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
```
|
|
2703
|
+
|
|
2704
|
+
## Modern Development Workflow
|
|
2705
|
+
|
|
2706
|
+
### Project Configuration
|
|
2707
|
+
|
|
2708
|
+
```yaml
|
|
2709
|
+
# pubspec.yaml
|
|
2710
|
+
name: my_flutter_app
|
|
2711
|
+
description: A comprehensive Flutter application
|
|
2712
|
+
publish_to: 'none'
|
|
2713
|
+
|
|
2714
|
+
version: 1.0.0+1
|
|
2715
|
+
|
|
2716
|
+
environment:
|
|
2717
|
+
sdk: '>=3.5.0 <4.0.0'
|
|
2718
|
+
flutter: ">=3.24.0"
|
|
2719
|
+
|
|
2720
|
+
dependencies:
|
|
2721
|
+
flutter:
|
|
2722
|
+
sdk: flutter
|
|
2723
|
+
|
|
2724
|
+
# State management
|
|
2725
|
+
flutter_riverpod: ^2.5.0
|
|
2726
|
+
provider: ^6.1.2
|
|
2727
|
+
|
|
2728
|
+
# Navigation
|
|
2729
|
+
go_router: ^13.2.0
|
|
2730
|
+
|
|
2731
|
+
# HTTP client
|
|
2732
|
+
dio: ^5.4.3+1
|
|
2733
|
+
retrofit: ^4.0.3
|
|
2734
|
+
json_annotation: ^4.8.1
|
|
2735
|
+
|
|
2736
|
+
# Local storage
|
|
2737
|
+
shared_preferences: ^2.2.3
|
|
2738
|
+
flutter_secure_storage: ^9.0.0
|
|
2739
|
+
|
|
2740
|
+
# Database
|
|
2741
|
+
sqflite: ^2.3.3
|
|
2742
|
+
drift: ^2.17.0
|
|
2743
|
+
|
|
2744
|
+
# Authentication
|
|
2745
|
+
local_auth: ^2.2.0
|
|
2746
|
+
|
|
2747
|
+
# UI components
|
|
2748
|
+
material_color_utilities: ^0.8.0
|
|
2749
|
+
flutter_svg: ^2.0.10+1
|
|
2750
|
+
|
|
2751
|
+
# Utilities
|
|
2752
|
+
uuid: ^4.4.0
|
|
2753
|
+
intl: ^0.19.0
|
|
2754
|
+
equatable: ^2.0.5
|
|
2755
|
+
json_serializable: ^6.7.1
|
|
2756
|
+
|
|
2757
|
+
# Testing
|
|
2758
|
+
flutter_test:
|
|
2759
|
+
sdk: flutter
|
|
2760
|
+
mockito: ^5.4.4
|
|
2761
|
+
build_runner: ^2.4.9
|
|
2762
|
+
retrofit_generator: ^8.0.6
|
|
2763
|
+
json_serializable: ^6.7.1
|
|
2764
|
+
drift_dev: ^2.17.0
|
|
2765
|
+
|
|
2766
|
+
dev_dependencies:
|
|
2767
|
+
flutter_test:
|
|
2768
|
+
sdk: flutter
|
|
2769
|
+
|
|
2770
|
+
# Code generation
|
|
2771
|
+
build_runner: ^2.4.9
|
|
2772
|
+
retrofit_generator: ^8.0.6
|
|
2773
|
+
json_serializable: ^6.7.1
|
|
2774
|
+
drift_dev: ^2.17.0
|
|
2775
|
+
|
|
2776
|
+
# Linting and formatting
|
|
2777
|
+
flutter_lints: ^4.0.0
|
|
2778
|
+
very_good_analysis: ^5.1.0
|
|
2779
|
+
|
|
2780
|
+
# Testing
|
|
2781
|
+
integration_test:
|
|
2782
|
+
sdk: flutter
|
|
2783
|
+
golden_toolkit: ^0.15.0
|
|
2784
|
+
network_image_mock: ^2.1.1
|
|
2785
|
+
|
|
2786
|
+
flutter:
|
|
2787
|
+
uses-material-design: true
|
|
2788
|
+
|
|
2789
|
+
assets:
|
|
2790
|
+
- assets/images/
|
|
2791
|
+
- assets/icons/
|
|
2792
|
+
- assets/config/
|
|
2793
|
+
|
|
2794
|
+
fonts:
|
|
2795
|
+
- family: Roboto
|
|
2796
|
+
fonts:
|
|
2797
|
+
- asset: fonts/Roboto-Regular.ttf
|
|
2798
|
+
- asset: fonts/Roboto-Bold.ttf
|
|
2799
|
+
weight: 700
|
|
2800
|
+
```
|
|
2801
|
+
|
|
2802
|
+
### Analysis Configuration
|
|
2803
|
+
|
|
2804
|
+
```yaml
|
|
2805
|
+
# analysis_options.yaml
|
|
2806
|
+
include: package:very_good_analysis/analysis_options.yaml
|
|
2807
|
+
|
|
2808
|
+
analyzer:
|
|
2809
|
+
exclude:
|
|
2810
|
+
- "**/*.g.dart"
|
|
2811
|
+
- "**/*.freezed.dart"
|
|
2812
|
+
|
|
2813
|
+
language:
|
|
2814
|
+
strict-casts: true
|
|
2815
|
+
strict-inference: true
|
|
2816
|
+
strict-raw-types: true
|
|
2817
|
+
|
|
2818
|
+
errors:
|
|
2819
|
+
invalid_annotation_target: ignore
|
|
2820
|
+
missing_required_param: error
|
|
2821
|
+
missing_return: error
|
|
2822
|
+
todo: ignore
|
|
2823
|
+
|
|
2824
|
+
linter:
|
|
2825
|
+
rules:
|
|
2826
|
+
# Additional rules beyond very_good_analysis
|
|
2827
|
+
prefer_single_quotes: true
|
|
2828
|
+
sort_constructors_first: true
|
|
2829
|
+
sort_unnamed_constructors_first: true
|
|
2830
|
+
always_declare_return_types: true
|
|
2831
|
+
avoid_print: true
|
|
2832
|
+
avoid_unnecessary_containers: true
|
|
2833
|
+
sized_box_for_whitespace: true
|
|
2834
|
+
use_key_in_widget_constructors: true
|
|
2835
|
+
prefer_const_constructors: true
|
|
2836
|
+
prefer_const_declarations: true
|
|
2837
|
+
prefer_const_literals_to_create_immutables: true
|
|
2838
|
+
avoid_web_libraries_in_flutter: true
|
|
2839
|
+
prefer_const_constructors_in_immutables: true
|
|
2840
|
+
prefer_final_fields: true
|
|
2841
|
+
use_full_hex_values_for_flutter_colors: true
|
|
2842
|
+
```
|
|
2843
|
+
|
|
2844
|
+
### CI/CD Configuration
|
|
2845
|
+
|
|
2846
|
+
```yaml
|
|
2847
|
+
# .github/workflows/flutter.yml
|
|
2848
|
+
name: Flutter CI/CD
|
|
2849
|
+
|
|
2850
|
+
on:
|
|
2851
|
+
push:
|
|
2852
|
+
branches: [ main, develop ]
|
|
2853
|
+
pull_request:
|
|
2854
|
+
branches: [ main ]
|
|
2855
|
+
|
|
2856
|
+
jobs:
|
|
2857
|
+
test:
|
|
2858
|
+
runs-on: ubuntu-latest
|
|
2859
|
+
|
|
2860
|
+
steps:
|
|
2861
|
+
- uses: actions/checkout@v4
|
|
2862
|
+
|
|
2863
|
+
- name: Setup Flutter
|
|
2864
|
+
uses: subosito/flutter-action@v2
|
|
2865
|
+
with:
|
|
2866
|
+
channel: stable
|
|
2867
|
+
flutter-version: '3.24.x'
|
|
2868
|
+
|
|
2869
|
+
- name: Install dependencies
|
|
2870
|
+
run: flutter pub get
|
|
2871
|
+
|
|
2872
|
+
- name: Generate code
|
|
2873
|
+
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
2874
|
+
|
|
2875
|
+
- name: Analyze code
|
|
2876
|
+
run: flutter analyze
|
|
2877
|
+
|
|
2878
|
+
- name: Run tests
|
|
2879
|
+
run: flutter test --coverage --test-randomize-ordering-seed random
|
|
2880
|
+
|
|
2881
|
+
- name: Upload coverage to Codecov
|
|
2882
|
+
uses: codecov/codecov-action@v3
|
|
2883
|
+
with:
|
|
2884
|
+
file: coverage/lcov.info
|
|
2885
|
+
|
|
2886
|
+
- name: Run widget tests
|
|
2887
|
+
run: flutter test integration_test/
|
|
2888
|
+
|
|
2889
|
+
build_android:
|
|
2890
|
+
needs: test
|
|
2891
|
+
runs-on: ubuntu-latest
|
|
2892
|
+
if: github.ref == 'refs/heads/main'
|
|
2893
|
+
|
|
2894
|
+
steps:
|
|
2895
|
+
- uses: actions/checkout@v4
|
|
2896
|
+
|
|
2897
|
+
- name: Setup Flutter
|
|
2898
|
+
uses: subosito/flutter-action@v2
|
|
2899
|
+
with:
|
|
2900
|
+
channel: stable
|
|
2901
|
+
flutter-version: '3.24.x'
|
|
2902
|
+
|
|
2903
|
+
- name: Install dependencies
|
|
2904
|
+
run: flutter pub get
|
|
2905
|
+
|
|
2906
|
+
- name: Generate code
|
|
2907
|
+
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
2908
|
+
|
|
2909
|
+
- name: Build APK
|
|
2910
|
+
run: flutter build apk --release
|
|
2911
|
+
|
|
2912
|
+
- name: Build App Bundle
|
|
2913
|
+
run: flutter build appbundle --release
|
|
2914
|
+
|
|
2915
|
+
- name: Upload APK
|
|
2916
|
+
uses: actions/upload-artifact@v3
|
|
2917
|
+
with:
|
|
2918
|
+
name: android-apk
|
|
2919
|
+
path: build/app/outputs/flutter-apk/app-release.apk
|
|
2920
|
+
|
|
2921
|
+
- name: Upload App Bundle
|
|
2922
|
+
uses: actions/upload-artifact@v3
|
|
2923
|
+
with:
|
|
2924
|
+
name: android-aab
|
|
2925
|
+
path: build/app/outputs/bundle/release/app-release.aab
|
|
2926
|
+
|
|
2927
|
+
build_ios:
|
|
2928
|
+
needs: test
|
|
2929
|
+
runs-on: macos-latest
|
|
2930
|
+
if: github.ref == 'refs/heads/main'
|
|
2931
|
+
|
|
2932
|
+
steps:
|
|
2933
|
+
- uses: actions/checkout@v4
|
|
2934
|
+
|
|
2935
|
+
- name: Setup Flutter
|
|
2936
|
+
uses: subosito/flutter-action@v2
|
|
2937
|
+
with:
|
|
2938
|
+
channel: stable
|
|
2939
|
+
flutter-version: '3.24.x'
|
|
2940
|
+
|
|
2941
|
+
- name: Install dependencies
|
|
2942
|
+
run: flutter pub get
|
|
2943
|
+
|
|
2944
|
+
- name: Generate code
|
|
2945
|
+
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
2946
|
+
|
|
2947
|
+
- name: Build iOS
|
|
2948
|
+
run: flutter build ios --release --no-codesign
|
|
2949
|
+
|
|
2950
|
+
- name: Upload iOS build
|
|
2951
|
+
uses: actions/upload-artifact@v3
|
|
2952
|
+
with:
|
|
2953
|
+
name: ios-build
|
|
2954
|
+
path: build/ios/iphoneos/Runner.app
|
|
2955
|
+
|
|
2956
|
+
build_web:
|
|
2957
|
+
needs: test
|
|
2958
|
+
runs-on: ubuntu-latest
|
|
2959
|
+
|
|
2960
|
+
steps:
|
|
2961
|
+
- uses: actions/checkout@v4
|
|
2962
|
+
|
|
2963
|
+
- name: Setup Flutter
|
|
2964
|
+
uses: subosito/flutter-action@v2
|
|
2965
|
+
with:
|
|
2966
|
+
channel: stable
|
|
2967
|
+
flutter-version: '3.24.x'
|
|
2968
|
+
|
|
2969
|
+
- name: Install dependencies
|
|
2970
|
+
run: flutter pub get
|
|
2971
|
+
|
|
2972
|
+
- name: Generate code
|
|
2973
|
+
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
2974
|
+
|
|
2975
|
+
- name: Build web
|
|
2976
|
+
run: flutter build web --release
|
|
2977
|
+
|
|
2978
|
+
- name: Deploy to GitHub Pages
|
|
2979
|
+
uses: peaceiris/actions-gh-pages@v3
|
|
2980
|
+
with:
|
|
2981
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
2982
|
+
publish_dir: build/web
|
|
2983
|
+
```
|
|
2984
|
+
|
|
2985
|
+
---
|
|
91
2986
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
2987
|
+
**Created by**: MoAI Language Skill Factory
|
|
2988
|
+
**Last Updated**: 2025-11-06
|
|
2989
|
+
**Version**: 2.0.0
|
|
2990
|
+
**Dart Target**: 3.5.x with Flutter 3.24.x and modern async patterns
|
|
95
2991
|
|
|
96
|
-
|
|
97
|
-
- Enable automatic validation by matching your linter with the language's official style guide.
|
|
98
|
-
- Fix test/build pipelines with reproducible commands in CI.
|
|
2992
|
+
This skill provides comprehensive Dart development guidance with 2025 best practices, covering everything from Flutter mobile applications to server-side development and real-time communication with WebSockets.
|