@wazir-dev/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +111 -0
- package/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +101 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/assets/composition-engine.mmd +34 -0
- package/assets/demo-script.sh +17 -0
- package/assets/logo-dark.svg +14 -0
- package/assets/logo.svg +14 -0
- package/assets/pipeline.mmd +39 -0
- package/assets/record-demo.sh +51 -0
- package/docs/README.md +51 -0
- package/docs/adapters/context-mode.md +60 -0
- package/docs/concepts/architecture.md +87 -0
- package/docs/concepts/artifact-model.md +60 -0
- package/docs/concepts/composition-engine.md +36 -0
- package/docs/concepts/indexing-and-recall.md +160 -0
- package/docs/concepts/observability.md +41 -0
- package/docs/concepts/roles-and-workflows.md +59 -0
- package/docs/concepts/terminology-policy.md +27 -0
- package/docs/getting-started/01-installation.md +78 -0
- package/docs/getting-started/02-first-run.md +102 -0
- package/docs/getting-started/03-adding-to-project.md +15 -0
- package/docs/getting-started/04-host-setup.md +15 -0
- package/docs/guides/ci-integration.md +15 -0
- package/docs/guides/creating-skills.md +15 -0
- package/docs/guides/expertise-module-authoring.md +15 -0
- package/docs/guides/hook-development.md +15 -0
- package/docs/guides/memory-and-learnings.md +34 -0
- package/docs/guides/multi-host-export.md +15 -0
- package/docs/guides/troubleshooting.md +101 -0
- package/docs/guides/writing-custom-roles.md +15 -0
- package/docs/plans/2026-03-15-cli-pipeline-integration-design.md +592 -0
- package/docs/plans/2026-03-15-cli-pipeline-integration-plan.md +598 -0
- package/docs/plans/2026-03-15-docs-enforcement-plan.md +238 -0
- package/docs/readmes/INDEX.md +99 -0
- package/docs/readmes/features/expertise/README.md +171 -0
- package/docs/readmes/features/exports/README.md +222 -0
- package/docs/readmes/features/hooks/README.md +103 -0
- package/docs/readmes/features/hooks/loop-cap-guard.md +133 -0
- package/docs/readmes/features/hooks/post-tool-capture.md +121 -0
- package/docs/readmes/features/hooks/post-tool-lint.md +130 -0
- package/docs/readmes/features/hooks/pre-compact-summary.md +122 -0
- package/docs/readmes/features/hooks/pre-tool-capture-route.md +100 -0
- package/docs/readmes/features/hooks/protected-path-write-guard.md +128 -0
- package/docs/readmes/features/hooks/session-start.md +119 -0
- package/docs/readmes/features/hooks/stop-handoff-harvest.md +125 -0
- package/docs/readmes/features/roles/README.md +157 -0
- package/docs/readmes/features/roles/clarifier.md +152 -0
- package/docs/readmes/features/roles/content-author.md +190 -0
- package/docs/readmes/features/roles/designer.md +193 -0
- package/docs/readmes/features/roles/executor.md +184 -0
- package/docs/readmes/features/roles/learner.md +210 -0
- package/docs/readmes/features/roles/planner.md +182 -0
- package/docs/readmes/features/roles/researcher.md +164 -0
- package/docs/readmes/features/roles/reviewer.md +184 -0
- package/docs/readmes/features/roles/specifier.md +162 -0
- package/docs/readmes/features/roles/verifier.md +215 -0
- package/docs/readmes/features/schemas/README.md +178 -0
- package/docs/readmes/features/skills/README.md +63 -0
- package/docs/readmes/features/skills/brainstorming.md +96 -0
- package/docs/readmes/features/skills/debugging.md +148 -0
- package/docs/readmes/features/skills/design.md +120 -0
- package/docs/readmes/features/skills/prepare-next.md +109 -0
- package/docs/readmes/features/skills/run-audit.md +159 -0
- package/docs/readmes/features/skills/scan-project.md +109 -0
- package/docs/readmes/features/skills/self-audit.md +176 -0
- package/docs/readmes/features/skills/tdd.md +137 -0
- package/docs/readmes/features/skills/using-skills.md +92 -0
- package/docs/readmes/features/skills/verification.md +120 -0
- package/docs/readmes/features/skills/writing-plans.md +104 -0
- package/docs/readmes/features/tooling/README.md +320 -0
- package/docs/readmes/features/workflows/README.md +186 -0
- package/docs/readmes/features/workflows/author.md +181 -0
- package/docs/readmes/features/workflows/clarify.md +154 -0
- package/docs/readmes/features/workflows/design-review.md +171 -0
- package/docs/readmes/features/workflows/design.md +169 -0
- package/docs/readmes/features/workflows/discover.md +162 -0
- package/docs/readmes/features/workflows/execute.md +173 -0
- package/docs/readmes/features/workflows/learn.md +167 -0
- package/docs/readmes/features/workflows/plan-review.md +165 -0
- package/docs/readmes/features/workflows/plan.md +170 -0
- package/docs/readmes/features/workflows/prepare-next.md +167 -0
- package/docs/readmes/features/workflows/review.md +169 -0
- package/docs/readmes/features/workflows/run-audit.md +191 -0
- package/docs/readmes/features/workflows/spec-challenge.md +159 -0
- package/docs/readmes/features/workflows/specify.md +160 -0
- package/docs/readmes/features/workflows/verify.md +177 -0
- package/docs/readmes/packages/README.md +50 -0
- package/docs/readmes/packages/ajv.md +117 -0
- package/docs/readmes/packages/context-mode.md +118 -0
- package/docs/readmes/packages/gray-matter.md +116 -0
- package/docs/readmes/packages/node-test.md +137 -0
- package/docs/readmes/packages/yaml.md +112 -0
- package/docs/reference/configuration-reference.md +159 -0
- package/docs/reference/expertise-index.md +52 -0
- package/docs/reference/git-flow.md +43 -0
- package/docs/reference/hooks.md +87 -0
- package/docs/reference/host-exports.md +50 -0
- package/docs/reference/launch-checklist.md +172 -0
- package/docs/reference/marketplace-listings.md +76 -0
- package/docs/reference/release-process.md +34 -0
- package/docs/reference/roles-reference.md +77 -0
- package/docs/reference/skills.md +33 -0
- package/docs/reference/templates.md +29 -0
- package/docs/reference/tooling-cli.md +94 -0
- package/docs/truth-claims.yaml +222 -0
- package/expertise/PROGRESS.md +63 -0
- package/expertise/README.md +18 -0
- package/expertise/antipatterns/PROGRESS.md +56 -0
- package/expertise/antipatterns/backend/api-design-antipatterns.md +1271 -0
- package/expertise/antipatterns/backend/auth-antipatterns.md +1195 -0
- package/expertise/antipatterns/backend/caching-antipatterns.md +622 -0
- package/expertise/antipatterns/backend/database-antipatterns.md +1038 -0
- package/expertise/antipatterns/backend/index.md +24 -0
- package/expertise/antipatterns/backend/microservices-antipatterns.md +850 -0
- package/expertise/antipatterns/code/architecture-antipatterns.md +919 -0
- package/expertise/antipatterns/code/async-antipatterns.md +622 -0
- package/expertise/antipatterns/code/code-smells.md +1186 -0
- package/expertise/antipatterns/code/dependency-antipatterns.md +1209 -0
- package/expertise/antipatterns/code/error-handling-antipatterns.md +1360 -0
- package/expertise/antipatterns/code/index.md +27 -0
- package/expertise/antipatterns/code/naming-and-abstraction.md +1118 -0
- package/expertise/antipatterns/code/state-management-antipatterns.md +1076 -0
- package/expertise/antipatterns/code/testing-antipatterns.md +1053 -0
- package/expertise/antipatterns/design/accessibility-antipatterns.md +1136 -0
- package/expertise/antipatterns/design/dark-patterns.md +1121 -0
- package/expertise/antipatterns/design/index.md +22 -0
- package/expertise/antipatterns/design/ui-antipatterns.md +1202 -0
- package/expertise/antipatterns/design/ux-antipatterns.md +680 -0
- package/expertise/antipatterns/frontend/css-layout-antipatterns.md +691 -0
- package/expertise/antipatterns/frontend/flutter-antipatterns.md +1827 -0
- package/expertise/antipatterns/frontend/index.md +23 -0
- package/expertise/antipatterns/frontend/mobile-antipatterns.md +573 -0
- package/expertise/antipatterns/frontend/react-antipatterns.md +1128 -0
- package/expertise/antipatterns/frontend/spa-antipatterns.md +1235 -0
- package/expertise/antipatterns/index.md +31 -0
- package/expertise/antipatterns/performance/index.md +20 -0
- package/expertise/antipatterns/performance/performance-antipatterns.md +1013 -0
- package/expertise/antipatterns/performance/premature-optimization.md +623 -0
- package/expertise/antipatterns/performance/scaling-antipatterns.md +785 -0
- package/expertise/antipatterns/process/ai-coding-antipatterns.md +853 -0
- package/expertise/antipatterns/process/code-review-antipatterns.md +656 -0
- package/expertise/antipatterns/process/deployment-antipatterns.md +920 -0
- package/expertise/antipatterns/process/index.md +23 -0
- package/expertise/antipatterns/process/technical-debt-antipatterns.md +647 -0
- package/expertise/antipatterns/security/index.md +20 -0
- package/expertise/antipatterns/security/secrets-antipatterns.md +849 -0
- package/expertise/antipatterns/security/security-theater.md +843 -0
- package/expertise/antipatterns/security/vulnerability-patterns.md +801 -0
- package/expertise/architecture/PROGRESS.md +70 -0
- package/expertise/architecture/data/caching-architecture.md +671 -0
- package/expertise/architecture/data/data-consistency.md +574 -0
- package/expertise/architecture/data/data-modeling.md +536 -0
- package/expertise/architecture/data/event-streams-and-queues.md +634 -0
- package/expertise/architecture/data/index.md +25 -0
- package/expertise/architecture/data/search-architecture.md +663 -0
- package/expertise/architecture/data/sql-vs-nosql.md +708 -0
- package/expertise/architecture/decisions/architecture-decision-records.md +640 -0
- package/expertise/architecture/decisions/build-vs-buy.md +616 -0
- package/expertise/architecture/decisions/index.md +23 -0
- package/expertise/architecture/decisions/monolith-to-microservices.md +790 -0
- package/expertise/architecture/decisions/technology-selection.md +616 -0
- package/expertise/architecture/distributed/cap-theorem-and-tradeoffs.md +800 -0
- package/expertise/architecture/distributed/circuit-breaker-bulkhead.md +741 -0
- package/expertise/architecture/distributed/consensus-and-coordination.md +796 -0
- package/expertise/architecture/distributed/distributed-systems-fundamentals.md +564 -0
- package/expertise/architecture/distributed/idempotency-and-retry.md +796 -0
- package/expertise/architecture/distributed/index.md +25 -0
- package/expertise/architecture/distributed/saga-pattern.md +797 -0
- package/expertise/architecture/foundations/architectural-thinking.md +460 -0
- package/expertise/architecture/foundations/coupling-and-cohesion.md +770 -0
- package/expertise/architecture/foundations/design-principles-solid.md +649 -0
- package/expertise/architecture/foundations/domain-driven-design.md +719 -0
- package/expertise/architecture/foundations/index.md +25 -0
- package/expertise/architecture/foundations/separation-of-concerns.md +472 -0
- package/expertise/architecture/foundations/twelve-factor-app.md +797 -0
- package/expertise/architecture/index.md +34 -0
- package/expertise/architecture/integration/api-design-graphql.md +638 -0
- package/expertise/architecture/integration/api-design-grpc.md +804 -0
- package/expertise/architecture/integration/api-design-rest.md +892 -0
- package/expertise/architecture/integration/index.md +25 -0
- package/expertise/architecture/integration/third-party-integration.md +795 -0
- package/expertise/architecture/integration/webhooks-and-callbacks.md +1152 -0
- package/expertise/architecture/integration/websockets-realtime.md +791 -0
- package/expertise/architecture/mobile-architecture/index.md +22 -0
- package/expertise/architecture/mobile-architecture/mobile-app-architecture.md +780 -0
- package/expertise/architecture/mobile-architecture/mobile-backend-for-frontend.md +670 -0
- package/expertise/architecture/mobile-architecture/offline-first.md +719 -0
- package/expertise/architecture/mobile-architecture/push-and-sync.md +782 -0
- package/expertise/architecture/patterns/cqrs-event-sourcing.md +717 -0
- package/expertise/architecture/patterns/event-driven.md +797 -0
- package/expertise/architecture/patterns/hexagonal-clean-architecture.md +870 -0
- package/expertise/architecture/patterns/index.md +27 -0
- package/expertise/architecture/patterns/layered-architecture.md +736 -0
- package/expertise/architecture/patterns/microservices.md +753 -0
- package/expertise/architecture/patterns/modular-monolith.md +692 -0
- package/expertise/architecture/patterns/monolith.md +626 -0
- package/expertise/architecture/patterns/plugin-architecture.md +735 -0
- package/expertise/architecture/patterns/serverless.md +780 -0
- package/expertise/architecture/scaling/database-scaling.md +615 -0
- package/expertise/architecture/scaling/feature-flags-and-rollouts.md +757 -0
- package/expertise/architecture/scaling/horizontal-vs-vertical.md +606 -0
- package/expertise/architecture/scaling/index.md +24 -0
- package/expertise/architecture/scaling/multi-tenancy.md +800 -0
- package/expertise/architecture/scaling/stateless-design.md +787 -0
- package/expertise/backend/embedded-firmware.md +625 -0
- package/expertise/backend/go.md +853 -0
- package/expertise/backend/index.md +24 -0
- package/expertise/backend/java-spring.md +448 -0
- package/expertise/backend/node-typescript.md +625 -0
- package/expertise/backend/python-fastapi.md +724 -0
- package/expertise/backend/rust.md +458 -0
- package/expertise/backend/solidity.md +711 -0
- package/expertise/composition-map.yaml +443 -0
- package/expertise/content/foundations/content-modeling.md +395 -0
- package/expertise/content/foundations/editorial-standards.md +449 -0
- package/expertise/content/foundations/index.md +24 -0
- package/expertise/content/foundations/microcopy.md +455 -0
- package/expertise/content/foundations/terminology-governance.md +509 -0
- package/expertise/content/index.md +34 -0
- package/expertise/content/patterns/accessibility-copy.md +518 -0
- package/expertise/content/patterns/index.md +24 -0
- package/expertise/content/patterns/notification-content.md +433 -0
- package/expertise/content/patterns/sample-content.md +486 -0
- package/expertise/content/patterns/state-copy.md +439 -0
- package/expertise/design/PROGRESS.md +58 -0
- package/expertise/design/disciplines/dark-mode-theming.md +577 -0
- package/expertise/design/disciplines/design-systems.md +595 -0
- package/expertise/design/disciplines/index.md +25 -0
- package/expertise/design/disciplines/information-architecture.md +800 -0
- package/expertise/design/disciplines/interaction-design.md +788 -0
- package/expertise/design/disciplines/responsive-design.md +552 -0
- package/expertise/design/disciplines/usability-testing.md +516 -0
- package/expertise/design/disciplines/user-research.md +792 -0
- package/expertise/design/foundations/accessibility-design.md +796 -0
- package/expertise/design/foundations/color-theory.md +797 -0
- package/expertise/design/foundations/iconography.md +795 -0
- package/expertise/design/foundations/index.md +26 -0
- package/expertise/design/foundations/motion-and-animation.md +653 -0
- package/expertise/design/foundations/rtl-design.md +585 -0
- package/expertise/design/foundations/spacing-and-layout.md +607 -0
- package/expertise/design/foundations/typography.md +800 -0
- package/expertise/design/foundations/visual-hierarchy.md +761 -0
- package/expertise/design/index.md +32 -0
- package/expertise/design/patterns/authentication-flows.md +474 -0
- package/expertise/design/patterns/content-consumption.md +789 -0
- package/expertise/design/patterns/data-display.md +618 -0
- package/expertise/design/patterns/e-commerce.md +1494 -0
- package/expertise/design/patterns/feedback-and-states.md +642 -0
- package/expertise/design/patterns/forms-and-input.md +819 -0
- package/expertise/design/patterns/gamification.md +801 -0
- package/expertise/design/patterns/index.md +31 -0
- package/expertise/design/patterns/microinteractions.md +449 -0
- package/expertise/design/patterns/navigation.md +800 -0
- package/expertise/design/patterns/notifications.md +705 -0
- package/expertise/design/patterns/onboarding.md +700 -0
- package/expertise/design/patterns/search-and-filter.md +601 -0
- package/expertise/design/patterns/settings-and-preferences.md +768 -0
- package/expertise/design/patterns/social-and-community.md +748 -0
- package/expertise/design/platforms/desktop-native.md +612 -0
- package/expertise/design/platforms/index.md +25 -0
- package/expertise/design/platforms/mobile-android.md +825 -0
- package/expertise/design/platforms/mobile-cross-platform.md +983 -0
- package/expertise/design/platforms/mobile-ios.md +699 -0
- package/expertise/design/platforms/tablet.md +794 -0
- package/expertise/design/platforms/web-dashboard.md +790 -0
- package/expertise/design/platforms/web-responsive.md +550 -0
- package/expertise/design/psychology/behavioral-nudges.md +449 -0
- package/expertise/design/psychology/cognitive-load.md +1191 -0
- package/expertise/design/psychology/error-psychology.md +778 -0
- package/expertise/design/psychology/index.md +22 -0
- package/expertise/design/psychology/persuasive-design.md +736 -0
- package/expertise/design/psychology/user-mental-models.md +623 -0
- package/expertise/design/tooling/open-pencil.md +266 -0
- package/expertise/frontend/angular.md +1073 -0
- package/expertise/frontend/desktop-electron.md +546 -0
- package/expertise/frontend/flutter.md +782 -0
- package/expertise/frontend/index.md +27 -0
- package/expertise/frontend/native-android.md +409 -0
- package/expertise/frontend/native-ios.md +490 -0
- package/expertise/frontend/react-native.md +1160 -0
- package/expertise/frontend/react.md +808 -0
- package/expertise/frontend/vue.md +1089 -0
- package/expertise/humanize/domain-rules-code.md +79 -0
- package/expertise/humanize/domain-rules-content.md +67 -0
- package/expertise/humanize/domain-rules-technical-docs.md +56 -0
- package/expertise/humanize/index.md +35 -0
- package/expertise/humanize/self-audit-checklist.md +87 -0
- package/expertise/humanize/sentence-patterns.md +218 -0
- package/expertise/humanize/vocabulary-blacklist.md +105 -0
- package/expertise/i18n/PROGRESS.md +65 -0
- package/expertise/i18n/advanced/accessibility-and-i18n.md +28 -0
- package/expertise/i18n/advanced/bidirectional-text-algorithm.md +38 -0
- package/expertise/i18n/advanced/complex-scripts.md +30 -0
- package/expertise/i18n/advanced/performance-and-i18n.md +27 -0
- package/expertise/i18n/advanced/testing-i18n.md +28 -0
- package/expertise/i18n/content/content-adaptation.md +23 -0
- package/expertise/i18n/content/locale-specific-formatting.md +23 -0
- package/expertise/i18n/content/machine-translation-integration.md +28 -0
- package/expertise/i18n/content/translation-management.md +29 -0
- package/expertise/i18n/foundations/date-time-calendars.md +67 -0
- package/expertise/i18n/foundations/i18n-architecture.md +272 -0
- package/expertise/i18n/foundations/locale-and-language-tags.md +79 -0
- package/expertise/i18n/foundations/numbers-currency-units.md +61 -0
- package/expertise/i18n/foundations/pluralization-and-gender.md +109 -0
- package/expertise/i18n/foundations/string-externalization.md +236 -0
- package/expertise/i18n/foundations/text-direction-bidi.md +241 -0
- package/expertise/i18n/foundations/unicode-and-encoding.md +86 -0
- package/expertise/i18n/index.md +38 -0
- package/expertise/i18n/platform/backend-i18n.md +31 -0
- package/expertise/i18n/platform/flutter-i18n.md +148 -0
- package/expertise/i18n/platform/native-android-i18n.md +36 -0
- package/expertise/i18n/platform/native-ios-i18n.md +36 -0
- package/expertise/i18n/platform/react-i18n.md +103 -0
- package/expertise/i18n/platform/web-css-i18n.md +81 -0
- package/expertise/i18n/rtl/arabic-specific.md +175 -0
- package/expertise/i18n/rtl/hebrew-specific.md +149 -0
- package/expertise/i18n/rtl/rtl-animations-and-transitions.md +111 -0
- package/expertise/i18n/rtl/rtl-forms-and-input.md +161 -0
- package/expertise/i18n/rtl/rtl-fundamentals.md +211 -0
- package/expertise/i18n/rtl/rtl-icons-and-images.md +181 -0
- package/expertise/i18n/rtl/rtl-layout-mirroring.md +252 -0
- package/expertise/i18n/rtl/rtl-navigation-and-gestures.md +107 -0
- package/expertise/i18n/rtl/rtl-testing-and-qa.md +147 -0
- package/expertise/i18n/rtl/rtl-typography.md +160 -0
- package/expertise/index.md +113 -0
- package/expertise/index.yaml +216 -0
- package/expertise/infrastructure/cloud-aws.md +597 -0
- package/expertise/infrastructure/cloud-gcp.md +599 -0
- package/expertise/infrastructure/cybersecurity.md +816 -0
- package/expertise/infrastructure/database-mongodb.md +447 -0
- package/expertise/infrastructure/database-postgres.md +400 -0
- package/expertise/infrastructure/devops-cicd.md +787 -0
- package/expertise/infrastructure/index.md +27 -0
- package/expertise/performance/PROGRESS.md +50 -0
- package/expertise/performance/backend/api-latency.md +1204 -0
- package/expertise/performance/backend/background-jobs.md +506 -0
- package/expertise/performance/backend/connection-pooling.md +1209 -0
- package/expertise/performance/backend/database-query-optimization.md +515 -0
- package/expertise/performance/backend/index.md +23 -0
- package/expertise/performance/backend/rate-limiting-and-throttling.md +971 -0
- package/expertise/performance/foundations/algorithmic-complexity.md +954 -0
- package/expertise/performance/foundations/caching-strategies.md +489 -0
- package/expertise/performance/foundations/concurrency-and-parallelism.md +847 -0
- package/expertise/performance/foundations/index.md +24 -0
- package/expertise/performance/foundations/measuring-and-profiling.md +440 -0
- package/expertise/performance/foundations/memory-management.md +964 -0
- package/expertise/performance/foundations/performance-budgets.md +1314 -0
- package/expertise/performance/index.md +31 -0
- package/expertise/performance/infrastructure/auto-scaling.md +1059 -0
- package/expertise/performance/infrastructure/cdn-and-edge.md +1081 -0
- package/expertise/performance/infrastructure/index.md +22 -0
- package/expertise/performance/infrastructure/load-balancing.md +1081 -0
- package/expertise/performance/infrastructure/observability.md +1079 -0
- package/expertise/performance/mobile/index.md +23 -0
- package/expertise/performance/mobile/mobile-animations.md +544 -0
- package/expertise/performance/mobile/mobile-memory-battery.md +416 -0
- package/expertise/performance/mobile/mobile-network.md +452 -0
- package/expertise/performance/mobile/mobile-rendering.md +599 -0
- package/expertise/performance/mobile/mobile-startup-time.md +505 -0
- package/expertise/performance/platform-specific/flutter-performance.md +647 -0
- package/expertise/performance/platform-specific/index.md +22 -0
- package/expertise/performance/platform-specific/node-performance.md +1307 -0
- package/expertise/performance/platform-specific/postgres-performance.md +1366 -0
- package/expertise/performance/platform-specific/react-performance.md +1403 -0
- package/expertise/performance/web/bundle-optimization.md +1239 -0
- package/expertise/performance/web/image-and-media.md +636 -0
- package/expertise/performance/web/index.md +24 -0
- package/expertise/performance/web/network-optimization.md +1133 -0
- package/expertise/performance/web/rendering-performance.md +1098 -0
- package/expertise/performance/web/ssr-and-hydration.md +918 -0
- package/expertise/performance/web/web-vitals.md +1374 -0
- package/expertise/quality/accessibility.md +985 -0
- package/expertise/quality/evidence-based-verification.md +499 -0
- package/expertise/quality/index.md +24 -0
- package/expertise/quality/ml-model-audit.md +614 -0
- package/expertise/quality/performance.md +600 -0
- package/expertise/quality/testing-api.md +891 -0
- package/expertise/quality/testing-mobile.md +496 -0
- package/expertise/quality/testing-web.md +849 -0
- package/expertise/security/PROGRESS.md +54 -0
- package/expertise/security/agentic-identity.md +540 -0
- package/expertise/security/compliance-frameworks.md +601 -0
- package/expertise/security/data/data-encryption.md +364 -0
- package/expertise/security/data/data-privacy-gdpr.md +692 -0
- package/expertise/security/data/database-security.md +1171 -0
- package/expertise/security/data/index.md +22 -0
- package/expertise/security/data/pii-handling.md +531 -0
- package/expertise/security/foundations/authentication.md +1041 -0
- package/expertise/security/foundations/authorization.md +603 -0
- package/expertise/security/foundations/cryptography.md +1001 -0
- package/expertise/security/foundations/index.md +25 -0
- package/expertise/security/foundations/owasp-top-10.md +1354 -0
- package/expertise/security/foundations/secrets-management.md +1217 -0
- package/expertise/security/foundations/secure-sdlc.md +700 -0
- package/expertise/security/foundations/supply-chain-security.md +698 -0
- package/expertise/security/index.md +31 -0
- package/expertise/security/infrastructure/cloud-security-aws.md +1296 -0
- package/expertise/security/infrastructure/cloud-security-gcp.md +1376 -0
- package/expertise/security/infrastructure/container-security.md +721 -0
- package/expertise/security/infrastructure/incident-response.md +1295 -0
- package/expertise/security/infrastructure/index.md +24 -0
- package/expertise/security/infrastructure/logging-and-monitoring.md +1618 -0
- package/expertise/security/infrastructure/network-security.md +1337 -0
- package/expertise/security/mobile/index.md +23 -0
- package/expertise/security/mobile/mobile-android-security.md +1218 -0
- package/expertise/security/mobile/mobile-binary-protection.md +1229 -0
- package/expertise/security/mobile/mobile-data-storage.md +1265 -0
- package/expertise/security/mobile/mobile-ios-security.md +1401 -0
- package/expertise/security/mobile/mobile-network-security.md +1520 -0
- package/expertise/security/smart-contract-security.md +594 -0
- package/expertise/security/testing/index.md +22 -0
- package/expertise/security/testing/penetration-testing.md +1258 -0
- package/expertise/security/testing/security-code-review.md +1765 -0
- package/expertise/security/testing/threat-modeling.md +1074 -0
- package/expertise/security/testing/vulnerability-scanning.md +1062 -0
- package/expertise/security/web/api-security.md +586 -0
- package/expertise/security/web/cors-and-headers.md +433 -0
- package/expertise/security/web/csrf.md +562 -0
- package/expertise/security/web/file-upload.md +1477 -0
- package/expertise/security/web/index.md +25 -0
- package/expertise/security/web/injection.md +1375 -0
- package/expertise/security/web/session-management.md +1101 -0
- package/expertise/security/web/xss.md +1158 -0
- package/exports/README.md +17 -0
- package/exports/hosts/claude/.claude/agents/clarifier.md +42 -0
- package/exports/hosts/claude/.claude/agents/content-author.md +63 -0
- package/exports/hosts/claude/.claude/agents/designer.md +55 -0
- package/exports/hosts/claude/.claude/agents/executor.md +55 -0
- package/exports/hosts/claude/.claude/agents/learner.md +51 -0
- package/exports/hosts/claude/.claude/agents/planner.md +53 -0
- package/exports/hosts/claude/.claude/agents/researcher.md +43 -0
- package/exports/hosts/claude/.claude/agents/reviewer.md +54 -0
- package/exports/hosts/claude/.claude/agents/specifier.md +47 -0
- package/exports/hosts/claude/.claude/agents/verifier.md +71 -0
- package/exports/hosts/claude/.claude/commands/author.md +42 -0
- package/exports/hosts/claude/.claude/commands/clarify.md +38 -0
- package/exports/hosts/claude/.claude/commands/design-review.md +46 -0
- package/exports/hosts/claude/.claude/commands/design.md +44 -0
- package/exports/hosts/claude/.claude/commands/discover.md +37 -0
- package/exports/hosts/claude/.claude/commands/execute.md +48 -0
- package/exports/hosts/claude/.claude/commands/learn.md +38 -0
- package/exports/hosts/claude/.claude/commands/plan-review.md +42 -0
- package/exports/hosts/claude/.claude/commands/plan.md +39 -0
- package/exports/hosts/claude/.claude/commands/prepare-next.md +37 -0
- package/exports/hosts/claude/.claude/commands/review.md +40 -0
- package/exports/hosts/claude/.claude/commands/run-audit.md +41 -0
- package/exports/hosts/claude/.claude/commands/spec-challenge.md +41 -0
- package/exports/hosts/claude/.claude/commands/specify.md +38 -0
- package/exports/hosts/claude/.claude/commands/verify.md +37 -0
- package/exports/hosts/claude/.claude/settings.json +34 -0
- package/exports/hosts/claude/CLAUDE.md +19 -0
- package/exports/hosts/claude/export.manifest.json +38 -0
- package/exports/hosts/claude/host-package.json +67 -0
- package/exports/hosts/codex/AGENTS.md +19 -0
- package/exports/hosts/codex/export.manifest.json +38 -0
- package/exports/hosts/codex/host-package.json +41 -0
- package/exports/hosts/cursor/.cursor/hooks.json +16 -0
- package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +19 -0
- package/exports/hosts/cursor/export.manifest.json +38 -0
- package/exports/hosts/cursor/host-package.json +42 -0
- package/exports/hosts/gemini/GEMINI.md +19 -0
- package/exports/hosts/gemini/export.manifest.json +38 -0
- package/exports/hosts/gemini/host-package.json +41 -0
- package/hooks/README.md +18 -0
- package/hooks/definitions/loop_cap_guard.yaml +21 -0
- package/hooks/definitions/post_tool_capture.yaml +24 -0
- package/hooks/definitions/pre_compact_summary.yaml +19 -0
- package/hooks/definitions/pre_tool_capture_route.yaml +19 -0
- package/hooks/definitions/protected_path_write_guard.yaml +19 -0
- package/hooks/definitions/session_start.yaml +19 -0
- package/hooks/definitions/stop_handoff_harvest.yaml +20 -0
- package/hooks/loop-cap-guard +17 -0
- package/hooks/post-tool-lint +36 -0
- package/hooks/protected-path-write-guard +17 -0
- package/hooks/session-start +41 -0
- package/llms-full.txt +2355 -0
- package/llms.txt +43 -0
- package/package.json +79 -0
- package/roles/README.md +20 -0
- package/roles/clarifier.md +42 -0
- package/roles/content-author.md +63 -0
- package/roles/designer.md +55 -0
- package/roles/executor.md +55 -0
- package/roles/learner.md +51 -0
- package/roles/planner.md +53 -0
- package/roles/researcher.md +43 -0
- package/roles/reviewer.md +54 -0
- package/roles/specifier.md +47 -0
- package/roles/verifier.md +71 -0
- package/schemas/README.md +24 -0
- package/schemas/accepted-learning.schema.json +20 -0
- package/schemas/author-artifact.schema.json +156 -0
- package/schemas/clarification.schema.json +19 -0
- package/schemas/design-artifact.schema.json +80 -0
- package/schemas/docs-claim.schema.json +18 -0
- package/schemas/export-manifest.schema.json +20 -0
- package/schemas/hook.schema.json +67 -0
- package/schemas/host-export-package.schema.json +18 -0
- package/schemas/implementation-plan.schema.json +19 -0
- package/schemas/proposed-learning.schema.json +19 -0
- package/schemas/research.schema.json +18 -0
- package/schemas/review.schema.json +29 -0
- package/schemas/run-manifest.schema.json +18 -0
- package/schemas/spec-challenge.schema.json +18 -0
- package/schemas/spec.schema.json +20 -0
- package/schemas/usage.schema.json +102 -0
- package/schemas/verification-proof.schema.json +29 -0
- package/schemas/wazir-manifest.schema.json +173 -0
- package/skills/README.md +40 -0
- package/skills/brainstorming/SKILL.md +77 -0
- package/skills/debugging/SKILL.md +50 -0
- package/skills/design/SKILL.md +61 -0
- package/skills/dispatching-parallel-agents/SKILL.md +128 -0
- package/skills/executing-plans/SKILL.md +70 -0
- package/skills/finishing-a-development-branch/SKILL.md +169 -0
- package/skills/humanize/SKILL.md +123 -0
- package/skills/init-pipeline/SKILL.md +124 -0
- package/skills/prepare-next/SKILL.md +20 -0
- package/skills/receiving-code-review/SKILL.md +123 -0
- package/skills/requesting-code-review/SKILL.md +105 -0
- package/skills/requesting-code-review/code-reviewer.md +108 -0
- package/skills/run-audit/SKILL.md +197 -0
- package/skills/scan-project/SKILL.md +41 -0
- package/skills/self-audit/SKILL.md +153 -0
- package/skills/subagent-driven-development/SKILL.md +154 -0
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +26 -0
- package/skills/subagent-driven-development/implementer-prompt.md +102 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/skills/tdd/SKILL.md +23 -0
- package/skills/using-git-worktrees/SKILL.md +163 -0
- package/skills/using-skills/SKILL.md +95 -0
- package/skills/verification/SKILL.md +22 -0
- package/skills/wazir/SKILL.md +463 -0
- package/skills/writing-plans/SKILL.md +30 -0
- package/skills/writing-skills/SKILL.md +157 -0
- package/skills/writing-skills/anthropic-best-practices.md +122 -0
- package/skills/writing-skills/persuasion-principles.md +50 -0
- package/templates/README.md +20 -0
- package/templates/artifacts/README.md +10 -0
- package/templates/artifacts/accepted-learning.md +19 -0
- package/templates/artifacts/accepted-learning.template.json +12 -0
- package/templates/artifacts/author.md +74 -0
- package/templates/artifacts/author.template.json +19 -0
- package/templates/artifacts/clarification.md +21 -0
- package/templates/artifacts/clarification.template.json +12 -0
- package/templates/artifacts/execute-notes.md +19 -0
- package/templates/artifacts/implementation-plan.md +21 -0
- package/templates/artifacts/implementation-plan.template.json +11 -0
- package/templates/artifacts/learning-proposal.md +19 -0
- package/templates/artifacts/next-run-handoff.md +21 -0
- package/templates/artifacts/plan-review.md +19 -0
- package/templates/artifacts/proposed-learning.template.json +12 -0
- package/templates/artifacts/research.md +21 -0
- package/templates/artifacts/research.template.json +12 -0
- package/templates/artifacts/review-findings.md +19 -0
- package/templates/artifacts/review.template.json +11 -0
- package/templates/artifacts/run-manifest.template.json +8 -0
- package/templates/artifacts/spec-challenge.md +19 -0
- package/templates/artifacts/spec-challenge.template.json +11 -0
- package/templates/artifacts/spec.md +21 -0
- package/templates/artifacts/spec.template.json +12 -0
- package/templates/artifacts/verification-proof.md +19 -0
- package/templates/artifacts/verification-proof.template.json +11 -0
- package/templates/examples/accepted-learning.example.json +14 -0
- package/templates/examples/author.example.json +152 -0
- package/templates/examples/clarification.example.json +15 -0
- package/templates/examples/docs-claim.example.json +8 -0
- package/templates/examples/export-manifest.example.json +7 -0
- package/templates/examples/host-export-package.example.json +11 -0
- package/templates/examples/implementation-plan.example.json +17 -0
- package/templates/examples/proposed-learning.example.json +13 -0
- package/templates/examples/research.example.json +15 -0
- package/templates/examples/research.example.md +6 -0
- package/templates/examples/review.example.json +17 -0
- package/templates/examples/run-manifest.example.json +9 -0
- package/templates/examples/spec-challenge.example.json +14 -0
- package/templates/examples/spec.example.json +21 -0
- package/templates/examples/verification-proof.example.json +21 -0
- package/templates/examples/wazir-manifest.example.yaml +65 -0
- package/templates/task-definition-schema.md +99 -0
- package/tooling/README.md +20 -0
- package/tooling/src/adapters/context-mode.js +50 -0
- package/tooling/src/capture/command.js +376 -0
- package/tooling/src/capture/store.js +99 -0
- package/tooling/src/capture/usage.js +270 -0
- package/tooling/src/checks/branches.js +50 -0
- package/tooling/src/checks/brand-truth.js +110 -0
- package/tooling/src/checks/changelog.js +231 -0
- package/tooling/src/checks/command-registry.js +36 -0
- package/tooling/src/checks/commits.js +102 -0
- package/tooling/src/checks/docs-drift.js +103 -0
- package/tooling/src/checks/docs-truth.js +201 -0
- package/tooling/src/checks/runtime-surface.js +156 -0
- package/tooling/src/cli.js +116 -0
- package/tooling/src/command-options.js +56 -0
- package/tooling/src/commands/validate.js +320 -0
- package/tooling/src/doctor/command.js +91 -0
- package/tooling/src/export/command.js +77 -0
- package/tooling/src/export/compiler.js +498 -0
- package/tooling/src/guards/loop-cap-guard.js +52 -0
- package/tooling/src/guards/protected-path-write-guard.js +67 -0
- package/tooling/src/index/command.js +152 -0
- package/tooling/src/index/storage.js +1061 -0
- package/tooling/src/index/summarizers.js +261 -0
- package/tooling/src/loaders.js +18 -0
- package/tooling/src/project-root.js +22 -0
- package/tooling/src/recall/command.js +225 -0
- package/tooling/src/schema-validator.js +30 -0
- package/tooling/src/state-root.js +40 -0
- package/tooling/src/status/command.js +71 -0
- package/wazir.manifest.yaml +135 -0
- package/workflows/README.md +19 -0
- package/workflows/author.md +42 -0
- package/workflows/clarify.md +38 -0
- package/workflows/design-review.md +46 -0
- package/workflows/design.md +44 -0
- package/workflows/discover.md +37 -0
- package/workflows/execute.md +48 -0
- package/workflows/learn.md +38 -0
- package/workflows/plan-review.md +42 -0
- package/workflows/plan.md +39 -0
- package/workflows/prepare-next.md +37 -0
- package/workflows/review.md +40 -0
- package/workflows/run-audit.md +41 -0
- package/workflows/spec-challenge.md +41 -0
- package/workflows/specify.md +38 -0
- package/workflows/verify.md +37 -0
|
@@ -0,0 +1,1827 @@
|
|
|
1
|
+
# Flutter & Dart Anti-Patterns
|
|
2
|
+
|
|
3
|
+
> Flutter's reactive, widget-based architecture is powerful but unforgiving. A single misplaced `setState`, a forgotten `dispose`, or a monolithic widget can cascade into jank, memory leaks, and unmaintainable spaghetti. These anti-patterns are drawn from real production incidents, community post-mortems, and the collective pain of the Flutter ecosystem. Knowing them prevents more bugs than any amount of best-practice documentation.
|
|
4
|
+
|
|
5
|
+
> **Domain:** Frontend
|
|
6
|
+
> **Anti-patterns covered:** 21
|
|
7
|
+
> **Highest severity:** Critical
|
|
8
|
+
|
|
9
|
+
## Anti-Patterns
|
|
10
|
+
|
|
11
|
+
### AP-01: The God Widget
|
|
12
|
+
|
|
13
|
+
**Also known as:** Monolithic Widget, Kitchen Sink Widget, The 800-Line build()
|
|
14
|
+
**Frequency:** Very Common
|
|
15
|
+
**Severity:** High
|
|
16
|
+
**Detection difficulty:** Easy
|
|
17
|
+
|
|
18
|
+
**What it looks like:**
|
|
19
|
+
|
|
20
|
+
A single widget file containing hundreds or thousands of lines, mixing layout, business logic, API calls, navigation, and state management in one `build()` method.
|
|
21
|
+
|
|
22
|
+
```dart
|
|
23
|
+
// BAD: 600-line widget doing everything
|
|
24
|
+
class DashboardScreen extends StatefulWidget {
|
|
25
|
+
@override
|
|
26
|
+
_DashboardScreenState createState() => _DashboardScreenState();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class _DashboardScreenState extends State<DashboardScreen> {
|
|
30
|
+
List<User> users = [];
|
|
31
|
+
bool isLoading = true;
|
|
32
|
+
String? error;
|
|
33
|
+
int selectedTab = 0;
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
void initState() {
|
|
37
|
+
super.initState();
|
|
38
|
+
fetchUsers();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Future<void> fetchUsers() async {
|
|
42
|
+
final response = await http.get(Uri.parse('https://api.example.com/users'));
|
|
43
|
+
// ... parsing logic here ...
|
|
44
|
+
setState(() { users = parsed; isLoading = false; });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
Widget build(BuildContext context) {
|
|
49
|
+
return Scaffold(
|
|
50
|
+
appBar: AppBar(/* 40 lines of app bar config */),
|
|
51
|
+
body: Column(
|
|
52
|
+
children: [
|
|
53
|
+
// 50 lines of tab bar
|
|
54
|
+
// 80 lines of search/filter UI
|
|
55
|
+
// 100 lines of list rendering
|
|
56
|
+
// 60 lines of bottom sheet logic
|
|
57
|
+
// 40 lines of dialogs
|
|
58
|
+
],
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Why developers do it:**
|
|
66
|
+
|
|
67
|
+
Flutter makes it easy to keep adding widgets inline. When prototyping, developers add "just one more widget" to the build method. There is no compile-time warning for large widgets, and the app still runs fine, so the problem creeps up gradually. Beginners coming from imperative frameworks often do not think in terms of composition.
|
|
68
|
+
|
|
69
|
+
**What goes wrong:**
|
|
70
|
+
|
|
71
|
+
- The entire widget tree rebuilds on every `setState`, even parts that did not change.
|
|
72
|
+
- Testing any single piece of functionality requires instantiating the entire screen.
|
|
73
|
+
- Multiple developers editing the same file causes merge conflicts constantly.
|
|
74
|
+
- Performance degrades as the widget tree grows -- Flutter DevTools rebuild stats show hundreds of unnecessary rebuilds per frame in severe cases.
|
|
75
|
+
- Code reuse becomes impossible; similar UI sections get copy-pasted.
|
|
76
|
+
|
|
77
|
+
**The fix:**
|
|
78
|
+
|
|
79
|
+
Extract sub-trees into separate `StatelessWidget` or `StatefulWidget` classes. Move business logic into services, repositories, or state management classes (BLoC, Riverpod, etc.).
|
|
80
|
+
|
|
81
|
+
```dart
|
|
82
|
+
// GOOD: Composed from small, focused widgets
|
|
83
|
+
class DashboardScreen extends StatelessWidget {
|
|
84
|
+
@override
|
|
85
|
+
Widget build(BuildContext context) {
|
|
86
|
+
return Scaffold(
|
|
87
|
+
appBar: const DashboardAppBar(),
|
|
88
|
+
body: Column(
|
|
89
|
+
children: [
|
|
90
|
+
const DashboardTabBar(),
|
|
91
|
+
const UserSearchBar(),
|
|
92
|
+
Expanded(child: UserListView()),
|
|
93
|
+
],
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Each sub-widget is its own class with focused responsibility
|
|
100
|
+
class UserListView extends StatelessWidget {
|
|
101
|
+
@override
|
|
102
|
+
Widget build(BuildContext context) {
|
|
103
|
+
final users = context.watch<UserProvider>().users;
|
|
104
|
+
return ListView.builder(
|
|
105
|
+
itemCount: users.length,
|
|
106
|
+
itemBuilder: (context, index) => UserCard(user: users[index]),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Detection rule:**
|
|
113
|
+
If a single widget file exceeds 200 lines or its `build()` method exceeds 80 lines, suspect AP-01. If a widget imports HTTP clients, databases, or service classes directly, it is almost certainly a God Widget.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### AP-02: Splitting Widgets into Methods Instead of Classes
|
|
118
|
+
|
|
119
|
+
**Also known as:** Helper Method Anti-Pattern, The Private Build Method Trap
|
|
120
|
+
**Frequency:** Very Common
|
|
121
|
+
**Severity:** Medium
|
|
122
|
+
**Detection difficulty:** Moderate
|
|
123
|
+
|
|
124
|
+
**What it looks like:**
|
|
125
|
+
|
|
126
|
+
Developers break up a large `build()` by extracting pieces into private methods like `_buildHeader()`, `_buildBody()`, `_buildFooter()` instead of creating separate widget classes.
|
|
127
|
+
|
|
128
|
+
```dart
|
|
129
|
+
// BAD: Methods instead of widgets
|
|
130
|
+
class ProfileScreen extends StatefulWidget {
|
|
131
|
+
@override
|
|
132
|
+
_ProfileScreenState createState() => _ProfileScreenState();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class _ProfileScreenState extends State<ProfileScreen> {
|
|
136
|
+
String name = 'User';
|
|
137
|
+
|
|
138
|
+
Widget _buildAvatar() {
|
|
139
|
+
return CircleAvatar(child: Text(name[0]));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Widget _buildStats() {
|
|
143
|
+
return Row(
|
|
144
|
+
children: [
|
|
145
|
+
Text('Posts: 42'),
|
|
146
|
+
Text('Followers: 100'),
|
|
147
|
+
],
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@override
|
|
152
|
+
Widget build(BuildContext context) {
|
|
153
|
+
return Column(
|
|
154
|
+
children: [
|
|
155
|
+
_buildAvatar(),
|
|
156
|
+
_buildStats(), // Rebuilds every time even though data hasn't changed
|
|
157
|
+
],
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Why developers do it:**
|
|
164
|
+
|
|
165
|
+
It looks cleaner than one massive `build()`. In other frameworks (React class components, Android XML), extracting helper methods is standard practice. IDE refactoring tools offer "Extract Method" as the first option. Developers assume methods and classes are interchangeable for this purpose.
|
|
166
|
+
|
|
167
|
+
**What goes wrong:**
|
|
168
|
+
|
|
169
|
+
When the parent calls `setState`, Flutter rebuilds the entire widget subtree. Methods are re-invoked unconditionally because Flutter has no way to know whether a method's output has changed. In contrast, a separate `StatelessWidget` with `const` constructor can be skipped entirely during rebuilds. This was documented extensively by Iiro Krankka in his widely-cited article "Splitting widgets to methods is a performance anti-pattern" and confirmed by the Flutter team. In complex screens with animations, this can cause visible jank on lower-end devices.
|
|
170
|
+
|
|
171
|
+
**The fix:**
|
|
172
|
+
|
|
173
|
+
```dart
|
|
174
|
+
// GOOD: Separate widget classes
|
|
175
|
+
class ProfileAvatar extends StatelessWidget {
|
|
176
|
+
final String name;
|
|
177
|
+
const ProfileAvatar({required this.name});
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
Widget build(BuildContext context) {
|
|
181
|
+
return CircleAvatar(child: Text(name[0]));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class ProfileStats extends StatelessWidget {
|
|
186
|
+
const ProfileStats();
|
|
187
|
+
|
|
188
|
+
@override
|
|
189
|
+
Widget build(BuildContext context) {
|
|
190
|
+
return const Row(
|
|
191
|
+
children: [
|
|
192
|
+
Text('Posts: 42'),
|
|
193
|
+
Text('Followers: 100'),
|
|
194
|
+
],
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Detection rule:**
|
|
201
|
+
If a `State` class contains private methods returning `Widget` (pattern: `Widget _build*(`), suspect AP-02. DCM lint rule `avoid-returning-widgets` catches this automatically.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### AP-03: Missing const Constructors
|
|
206
|
+
|
|
207
|
+
**Also known as:** The Rebuild Tax, Const Blindness
|
|
208
|
+
**Frequency:** Very Common
|
|
209
|
+
**Severity:** Medium
|
|
210
|
+
**Detection difficulty:** Easy
|
|
211
|
+
|
|
212
|
+
**What it looks like:**
|
|
213
|
+
|
|
214
|
+
Widgets that could be `const` are instantiated without the `const` keyword, forcing Flutter to rebuild them on every parent rebuild.
|
|
215
|
+
|
|
216
|
+
```dart
|
|
217
|
+
// BAD: No const, rebuilds every time parent rebuilds
|
|
218
|
+
class MyApp extends StatelessWidget {
|
|
219
|
+
@override
|
|
220
|
+
Widget build(BuildContext context) {
|
|
221
|
+
return MaterialApp(
|
|
222
|
+
home: Scaffold(
|
|
223
|
+
appBar: AppBar(title: Text('My App')), // Not const
|
|
224
|
+
body: Center(
|
|
225
|
+
child: Padding(
|
|
226
|
+
padding: EdgeInsets.all(16.0), // Not const
|
|
227
|
+
child: Text('Hello World'), // Not const
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Why developers do it:**
|
|
237
|
+
|
|
238
|
+
It still compiles and runs without `const`. Many developers coming from other languages are not accustomed to `const` at the expression level. Before Dart 2.12+ lints, there was no automated warning. Some developers believe `const` is only a style preference, not a performance mechanism.
|
|
239
|
+
|
|
240
|
+
**What goes wrong:**
|
|
241
|
+
|
|
242
|
+
Without `const`, Flutter allocates a new widget object on every rebuild and must diff it against the previous tree. With `const`, Flutter recognizes the widget as identical to the previous build via canonical instance comparison and skips the entire subtree. In apps with frequent rebuilds (animations, scrolling, real-time data), this compounds into measurable frame drops. The Dart VM also loses compile-time optimizations: constant folding, pre-allocated memory, and reduced garbage collection pressure.
|
|
243
|
+
|
|
244
|
+
**The fix:**
|
|
245
|
+
|
|
246
|
+
```dart
|
|
247
|
+
// GOOD: const everywhere possible
|
|
248
|
+
class MyApp extends StatelessWidget {
|
|
249
|
+
const MyApp();
|
|
250
|
+
|
|
251
|
+
@override
|
|
252
|
+
Widget build(BuildContext context) {
|
|
253
|
+
return MaterialApp(
|
|
254
|
+
home: Scaffold(
|
|
255
|
+
appBar: AppBar(title: const Text('My App')),
|
|
256
|
+
body: const Center(
|
|
257
|
+
child: Padding(
|
|
258
|
+
padding: EdgeInsets.all(16.0),
|
|
259
|
+
child: Text('Hello World'),
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Detection rule:**
|
|
269
|
+
Enable `prefer_const_constructors`, `prefer_const_declarations`, and `prefer_const_literals_to_create_immutables` lint rules. Any widget whose constructor arguments are all compile-time constants but is instantiated without `const` is AP-03.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### AP-04: setState for Everything
|
|
274
|
+
|
|
275
|
+
**Also known as:** setState Hell, Prop Drilling Nightmare, The Scalability Wall
|
|
276
|
+
**Frequency:** Very Common
|
|
277
|
+
**Severity:** High
|
|
278
|
+
**Detection difficulty:** Moderate
|
|
279
|
+
|
|
280
|
+
**What it looks like:**
|
|
281
|
+
|
|
282
|
+
Using `setState` as the sole state management approach across the entire app, passing callbacks and data through multiple widget layers.
|
|
283
|
+
|
|
284
|
+
```dart
|
|
285
|
+
// BAD: setState cascading through the entire tree
|
|
286
|
+
class ShoppingApp extends StatefulWidget {
|
|
287
|
+
@override
|
|
288
|
+
_ShoppingAppState createState() => _ShoppingAppState();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
class _ShoppingAppState extends State<ShoppingApp> {
|
|
292
|
+
List<CartItem> cart = [];
|
|
293
|
+
User? currentUser;
|
|
294
|
+
String selectedCategory = 'all';
|
|
295
|
+
bool isDarkMode = false;
|
|
296
|
+
|
|
297
|
+
void addToCart(Product p) {
|
|
298
|
+
setState(() { cart.add(CartItem(product: p, qty: 1)); });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@override
|
|
302
|
+
Widget build(BuildContext context) {
|
|
303
|
+
return MaterialApp(
|
|
304
|
+
home: HomePage(
|
|
305
|
+
cart: cart,
|
|
306
|
+
user: currentUser,
|
|
307
|
+
selectedCategory: selectedCategory,
|
|
308
|
+
isDarkMode: isDarkMode,
|
|
309
|
+
onAddToCart: addToCart, // Drilled 3 levels deep
|
|
310
|
+
onCategoryChange: (c) => setState(() => selectedCategory = c),
|
|
311
|
+
onThemeToggle: () => setState(() => isDarkMode = !isDarkMode),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Why developers do it:**
|
|
319
|
+
|
|
320
|
+
`setState` is the first state management tool taught in every Flutter tutorial. It works perfectly for simple apps and prototypes. Adding a state management library (Riverpod, BLoC, Provider) feels like over-engineering for small projects, so developers keep using `setState` as the app grows.
|
|
321
|
+
|
|
322
|
+
**What goes wrong:**
|
|
323
|
+
|
|
324
|
+
- Every `setState` rebuilds the entire subtree from that widget down, even if only one piece of data changed.
|
|
325
|
+
- Prop drilling (passing data through widgets that do not use it) makes the code brittle and hard to refactor.
|
|
326
|
+
- Business logic becomes entangled with UI code, making unit testing impossible without widget tests.
|
|
327
|
+
- When two distant widgets need the same state, the state must be lifted to a common ancestor, often the root, causing the entire app to rebuild on any state change.
|
|
328
|
+
- Teams report that apps using only `setState` beyond 15-20 screens become effectively unmaintainable.
|
|
329
|
+
|
|
330
|
+
**The fix:**
|
|
331
|
+
|
|
332
|
+
Use `setState` only for truly local UI state (a toggle, a text field focus, an animation). Use a state management solution for shared or complex state.
|
|
333
|
+
|
|
334
|
+
```dart
|
|
335
|
+
// GOOD: Riverpod for shared state, setState for local UI state
|
|
336
|
+
final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>(
|
|
337
|
+
(ref) => CartNotifier(),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
class CartNotifier extends StateNotifier<List<CartItem>> {
|
|
341
|
+
CartNotifier() : super([]);
|
|
342
|
+
|
|
343
|
+
void add(Product p) {
|
|
344
|
+
state = [...state, CartItem(product: p, qty: 1)];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Widget only rebuilds when cart changes
|
|
349
|
+
class CartBadge extends ConsumerWidget {
|
|
350
|
+
const CartBadge();
|
|
351
|
+
|
|
352
|
+
@override
|
|
353
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
354
|
+
final count = ref.watch(cartProvider).length;
|
|
355
|
+
return Badge(label: Text('$count'));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Detection rule:**
|
|
361
|
+
If `setState` is called more than 5 times in a single `State` class, or if callback functions are passed through more than 2 widget levels, suspect AP-04. If `setState` modifies data that conceptually belongs to the application (not the widget), it is AP-04.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### AP-05: Forgetting to Dispose Controllers and Streams
|
|
366
|
+
|
|
367
|
+
**Also known as:** The Silent Memory Leak, Zombie Subscriptions, Disposal Amnesia
|
|
368
|
+
**Frequency:** Very Common
|
|
369
|
+
**Severity:** Critical
|
|
370
|
+
**Detection difficulty:** Hard
|
|
371
|
+
|
|
372
|
+
**What it looks like:**
|
|
373
|
+
|
|
374
|
+
Creating `TextEditingController`, `AnimationController`, `ScrollController`, `StreamSubscription`, or `Timer` in a `StatefulWidget` without calling `dispose()` or `cancel()`.
|
|
375
|
+
|
|
376
|
+
```dart
|
|
377
|
+
// BAD: Controllers never disposed
|
|
378
|
+
class SearchScreen extends StatefulWidget {
|
|
379
|
+
@override
|
|
380
|
+
_SearchScreenState createState() => _SearchScreenState();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
class _SearchScreenState extends State<SearchScreen>
|
|
384
|
+
with SingleTickerProviderStateMixin {
|
|
385
|
+
final searchController = TextEditingController();
|
|
386
|
+
final scrollController = ScrollController();
|
|
387
|
+
late AnimationController animController;
|
|
388
|
+
late StreamSubscription<LocationData> locationSub;
|
|
389
|
+
|
|
390
|
+
@override
|
|
391
|
+
void initState() {
|
|
392
|
+
super.initState();
|
|
393
|
+
animController = AnimationController(vsync: this, duration: Duration(seconds: 1));
|
|
394
|
+
locationSub = locationService.stream.listen((data) {
|
|
395
|
+
setState(() { /* update location */ });
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// No dispose() method!
|
|
400
|
+
|
|
401
|
+
@override
|
|
402
|
+
Widget build(BuildContext context) => /* ... */;
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Why developers do it:**
|
|
407
|
+
|
|
408
|
+
The app appears to work fine in development because the garbage collector eventually reclaims some objects, and short debugging sessions do not reveal the leak. Developers forget because `initState` and `dispose` are not symmetrically enforced by the compiler. Stream subscriptions are particularly easy to forget because `listen()` returns the subscription as a value that developers often ignore.
|
|
409
|
+
|
|
410
|
+
**What goes wrong:**
|
|
411
|
+
|
|
412
|
+
- `TextEditingController` and `ScrollController` leak native resources and listeners.
|
|
413
|
+
- `AnimationController` continues ticking after the widget is removed, consuming CPU and eventually crashing with "setState called after dispose."
|
|
414
|
+
- `StreamSubscription` keeps firing callbacks on a disposed widget, causing crashes or corrupted state.
|
|
415
|
+
- In navigation-heavy apps, every push/pop cycle leaks more memory. Users on low-end Android devices report apps slowing to a crawl after 10-15 minutes of use.
|
|
416
|
+
- DCM's blog on memory leaks documents cases where undisposed stream subscriptions caused apps to consume 500MB+ of memory within minutes of normal use.
|
|
417
|
+
|
|
418
|
+
**The fix:**
|
|
419
|
+
|
|
420
|
+
```dart
|
|
421
|
+
// GOOD: Always dispose in dispose()
|
|
422
|
+
class _SearchScreenState extends State<SearchScreen>
|
|
423
|
+
with SingleTickerProviderStateMixin {
|
|
424
|
+
final searchController = TextEditingController();
|
|
425
|
+
final scrollController = ScrollController();
|
|
426
|
+
late AnimationController animController;
|
|
427
|
+
late StreamSubscription<LocationData> locationSub;
|
|
428
|
+
|
|
429
|
+
@override
|
|
430
|
+
void initState() {
|
|
431
|
+
super.initState();
|
|
432
|
+
animController = AnimationController(vsync: this, duration: Duration(seconds: 1));
|
|
433
|
+
locationSub = locationService.stream.listen((data) {
|
|
434
|
+
if (mounted) setState(() { /* update */ });
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@override
|
|
439
|
+
void dispose() {
|
|
440
|
+
searchController.dispose();
|
|
441
|
+
scrollController.dispose();
|
|
442
|
+
animController.dispose();
|
|
443
|
+
locationSub.cancel();
|
|
444
|
+
super.dispose();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@override
|
|
448
|
+
Widget build(BuildContext context) => /* ... */;
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Detection rule:**
|
|
453
|
+
If a `State` class creates a `TextEditingController`, `AnimationController`, `ScrollController`, `FocusNode`, `StreamSubscription`, or `Timer` in `initState` or field initialization, but has no `dispose()` method (or the `dispose()` method does not reference that object), it is AP-05. Lint rule: `close_sinks`, `cancel_subscriptions`.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### AP-06: Blocking the UI Thread with Heavy Computation
|
|
458
|
+
|
|
459
|
+
**Also known as:** Main Thread Abuse, The Jank Generator, Synchronous Sin
|
|
460
|
+
**Frequency:** Common
|
|
461
|
+
**Severity:** Critical
|
|
462
|
+
**Detection difficulty:** Moderate
|
|
463
|
+
|
|
464
|
+
**What it looks like:**
|
|
465
|
+
|
|
466
|
+
Running expensive computation, large JSON parsing, image processing, or file I/O directly in the `build()` method or in response to user interaction on the main isolate.
|
|
467
|
+
|
|
468
|
+
```dart
|
|
469
|
+
// BAD: Heavy computation on main thread
|
|
470
|
+
class ReportScreen extends StatefulWidget {
|
|
471
|
+
@override
|
|
472
|
+
_ReportScreenState createState() => _ReportScreenState();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
class _ReportScreenState extends State<ReportScreen> {
|
|
476
|
+
List<ChartData> chartData = [];
|
|
477
|
+
|
|
478
|
+
void generateReport() {
|
|
479
|
+
// This blocks the UI for 2-3 seconds
|
|
480
|
+
final rawData = loadCsvSync('large_dataset.csv'); // 50MB file
|
|
481
|
+
final parsed = rawData.split('\n').map((line) {
|
|
482
|
+
final cols = line.split(',');
|
|
483
|
+
return ChartData(
|
|
484
|
+
x: double.parse(cols[0]),
|
|
485
|
+
y: double.parse(cols[1]),
|
|
486
|
+
label: cols[2],
|
|
487
|
+
);
|
|
488
|
+
}).toList();
|
|
489
|
+
|
|
490
|
+
// Sort, filter, aggregate -- all on main thread
|
|
491
|
+
parsed.sort((a, b) => a.x.compareTo(b.x));
|
|
492
|
+
setState(() { chartData = parsed; });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
@override
|
|
496
|
+
Widget build(BuildContext context) => /* ... */;
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Why developers do it:**
|
|
501
|
+
|
|
502
|
+
Dart's `async`/`await` syntax looks like it solves concurrency, but `async` in Dart is cooperative concurrency on a single thread, not parallelism. A `Future` still runs on the main isolate. Developers assume `await` means "run in background" when it actually means "yield the event loop but still use the same thread." The distinction between concurrency and parallelism is not well understood.
|
|
503
|
+
|
|
504
|
+
**What goes wrong:**
|
|
505
|
+
|
|
506
|
+
- Flutter targets 60fps (16ms per frame). Any computation exceeding 16ms causes dropped frames visible as jank.
|
|
507
|
+
- On low-end Android devices, even 5ms of synchronous work in `build()` can cause visible stutter.
|
|
508
|
+
- Users perceive the app as frozen; if the main thread is blocked for more than 5 seconds on Android, the OS shows an ANR (Application Not Responding) dialog.
|
|
509
|
+
- Flutter GitHub issue #121720 documents cases where even isolate spawning can briefly block UI if not managed correctly.
|
|
510
|
+
|
|
511
|
+
**The fix:**
|
|
512
|
+
|
|
513
|
+
Use `Isolate.run()` (Dart 2.19+) or `compute()` for CPU-intensive work.
|
|
514
|
+
|
|
515
|
+
```dart
|
|
516
|
+
// GOOD: Heavy work in a separate isolate
|
|
517
|
+
Future<void> generateReport() async {
|
|
518
|
+
setState(() { isLoading = true; });
|
|
519
|
+
|
|
520
|
+
final chartData = await Isolate.run(() {
|
|
521
|
+
final rawData = File('large_dataset.csv').readAsStringSync();
|
|
522
|
+
final parsed = rawData.split('\n').map((line) {
|
|
523
|
+
final cols = line.split(',');
|
|
524
|
+
return ChartData(
|
|
525
|
+
x: double.parse(cols[0]),
|
|
526
|
+
y: double.parse(cols[1]),
|
|
527
|
+
label: cols[2],
|
|
528
|
+
);
|
|
529
|
+
}).toList();
|
|
530
|
+
parsed.sort((a, b) => a.x.compareTo(b.x));
|
|
531
|
+
return parsed;
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
setState(() { this.chartData = chartData; isLoading = false; });
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Detection rule:**
|
|
539
|
+
If `build()` contains loops processing more than a trivial number of items, file I/O, JSON parsing of unbounded data, or sorting of large collections, suspect AP-06. If a synchronous method is called that could take >16ms, it is AP-06. Look for `readAsStringSync`, `jsonDecode` on large payloads, or nested loops inside widget methods.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
### AP-07: Excessive GlobalKey Usage
|
|
544
|
+
|
|
545
|
+
**Also known as:** GlobalKey Overload, The Expensive Identity
|
|
546
|
+
**Frequency:** Common
|
|
547
|
+
**Severity:** Medium
|
|
548
|
+
**Detection difficulty:** Moderate
|
|
549
|
+
|
|
550
|
+
**What it looks like:**
|
|
551
|
+
|
|
552
|
+
Using `GlobalKey` for tasks that could be accomplished with `ValueKey`, `UniqueKey`, or no key at all. Creating `GlobalKey` instances inside `build()`.
|
|
553
|
+
|
|
554
|
+
```dart
|
|
555
|
+
// BAD: GlobalKey created in build -- destroys and recreates subtree every build
|
|
556
|
+
class MyWidget extends StatelessWidget {
|
|
557
|
+
@override
|
|
558
|
+
Widget build(BuildContext context) {
|
|
559
|
+
return Form(
|
|
560
|
+
key: GlobalKey<FormState>(), // NEW key every build!
|
|
561
|
+
child: Column(/* ... */),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// BAD: GlobalKey used just to read a value
|
|
567
|
+
class ParentWidget extends StatefulWidget {
|
|
568
|
+
@override
|
|
569
|
+
_ParentWidgetState createState() => _ParentWidgetState();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
class _ParentWidgetState extends State<ParentWidget> {
|
|
573
|
+
final _childKey = GlobalKey<_ChildWidgetState>();
|
|
574
|
+
|
|
575
|
+
void doSomething() {
|
|
576
|
+
// Reaching into child state via GlobalKey
|
|
577
|
+
_childKey.currentState?.someValue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@override
|
|
581
|
+
Widget build(BuildContext context) {
|
|
582
|
+
return ChildWidget(key: _childKey);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Why developers do it:**
|
|
588
|
+
|
|
589
|
+
`GlobalKey` provides direct access to a widget's `State`, which feels convenient for cross-widget communication. Tutorials for `Form` validation use `GlobalKey<FormState>()` prominently, leading developers to assume GlobalKey is the standard approach for all inter-widget communication.
|
|
590
|
+
|
|
591
|
+
**What goes wrong:**
|
|
592
|
+
|
|
593
|
+
- Creating `GlobalKey` in `build()` throws away the entire subtree state on every rebuild, as documented in Flutter's official API docs. A `GestureDetector` in that subtree loses ongoing gesture tracking. Text fields lose their content.
|
|
594
|
+
- Each `GlobalKey` is registered in a global lookup table, increasing memory consumption linearly with the number of keys.
|
|
595
|
+
- Flutter issue #35730 documents the performance overhead: GlobalKeys are more expensive than other keys because they require global uniqueness checks.
|
|
596
|
+
- Using GlobalKey to access child state creates tight coupling and makes refactoring dangerous.
|
|
597
|
+
|
|
598
|
+
**The fix:**
|
|
599
|
+
|
|
600
|
+
Instantiate GlobalKey in `initState` or as a field, never in `build()`. Prefer callbacks, state management, or `ValueKey`/`UniqueKey` where GlobalKey is not strictly needed.
|
|
601
|
+
|
|
602
|
+
```dart
|
|
603
|
+
// GOOD: GlobalKey created once, outside build
|
|
604
|
+
class _MyFormState extends State<MyForm> {
|
|
605
|
+
final _formKey = GlobalKey<FormState>(); // Created once
|
|
606
|
+
|
|
607
|
+
@override
|
|
608
|
+
Widget build(BuildContext context) {
|
|
609
|
+
return Form(
|
|
610
|
+
key: _formKey,
|
|
611
|
+
child: Column(/* ... */),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// BETTER: Use callbacks instead of reaching into child state
|
|
617
|
+
class ParentWidget extends StatefulWidget {
|
|
618
|
+
@override
|
|
619
|
+
_ParentWidgetState createState() => _ParentWidgetState();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
class _ParentWidgetState extends State<ParentWidget> {
|
|
623
|
+
String? childValue;
|
|
624
|
+
|
|
625
|
+
@override
|
|
626
|
+
Widget build(BuildContext context) {
|
|
627
|
+
return ChildWidget(
|
|
628
|
+
onValueChanged: (v) => setState(() => childValue = v),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
**Detection rule:**
|
|
635
|
+
If `GlobalKey()` appears inside a `build()` method, it is always AP-07. If `GlobalKey` is used to access child state and a callback pattern could replace it, it is AP-07. Count GlobalKeys per screen: more than 2-3 is a code smell.
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
### AP-08: Missing Keys in Lists
|
|
640
|
+
|
|
641
|
+
**Also known as:** The Phantom Reorder, State Mismatch Bug, Keyless Lists
|
|
642
|
+
**Frequency:** Common
|
|
643
|
+
**Severity:** High
|
|
644
|
+
**Detection difficulty:** Hard
|
|
645
|
+
|
|
646
|
+
**What it looks like:**
|
|
647
|
+
|
|
648
|
+
Building lists of stateful widgets without providing keys, or using the list index as the key.
|
|
649
|
+
|
|
650
|
+
```dart
|
|
651
|
+
// BAD: No keys -- state gets confused when items reorder
|
|
652
|
+
ListView.builder(
|
|
653
|
+
itemCount: todos.length,
|
|
654
|
+
itemBuilder: (context, index) {
|
|
655
|
+
return TodoTile(todo: todos[index]); // No key!
|
|
656
|
+
},
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
// ALSO BAD: Index as key -- same problem when items are removed/reordered
|
|
660
|
+
ListView.builder(
|
|
661
|
+
itemCount: todos.length,
|
|
662
|
+
itemBuilder: (context, index) {
|
|
663
|
+
return TodoTile(
|
|
664
|
+
key: ValueKey(index), // Index changes when items reorder!
|
|
665
|
+
todo: todos[index],
|
|
666
|
+
);
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Why developers do it:**
|
|
672
|
+
|
|
673
|
+
Keys are not required by the compiler, and lists appear to work correctly until items are reordered, deleted, or inserted. The bug manifests as "the checkbox state moved to the wrong item," which developers blame on their state management rather than missing keys.
|
|
674
|
+
|
|
675
|
+
**What goes wrong:**
|
|
676
|
+
|
|
677
|
+
- Flutter matches old and new widgets by position in the list. When an item is removed from the middle, all subsequent widgets inherit the state of their predecessor.
|
|
678
|
+
- Checkboxes appear checked on wrong items, text fields show wrong content, animations play on wrong elements.
|
|
679
|
+
- This is one of the most frequently reported "unexplainable bugs" in the Flutter community, as noted on FlutterClutter and multiple Stack Overflow threads.
|
|
680
|
+
- Using index as key is equally broken: if item at index 2 is deleted, the old index-3 item now has key 2 and inherits index-2's state.
|
|
681
|
+
|
|
682
|
+
**The fix:**
|
|
683
|
+
|
|
684
|
+
```dart
|
|
685
|
+
// GOOD: Stable, unique key per item
|
|
686
|
+
ListView.builder(
|
|
687
|
+
itemCount: todos.length,
|
|
688
|
+
itemBuilder: (context, index) {
|
|
689
|
+
return TodoTile(
|
|
690
|
+
key: ValueKey(todos[index].id), // Stable unique identifier
|
|
691
|
+
todo: todos[index],
|
|
692
|
+
);
|
|
693
|
+
},
|
|
694
|
+
);
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Detection rule:**
|
|
698
|
+
If a `ListView.builder`, `Column`, or `Row` renders stateful widgets without a `key` parameter, suspect AP-08. If `ValueKey(index)` is used where the list supports reordering or deletion, it is AP-08.
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
### AP-09: Using BuildContext Across Async Gaps
|
|
703
|
+
|
|
704
|
+
**Also known as:** Stale Context, The Mounted Check Omission, Context After Dispose
|
|
705
|
+
**Frequency:** Very Common
|
|
706
|
+
**Severity:** Critical
|
|
707
|
+
**Detection difficulty:** Moderate
|
|
708
|
+
|
|
709
|
+
**What it looks like:**
|
|
710
|
+
|
|
711
|
+
Using `BuildContext` after an `await` call without checking if the widget is still mounted.
|
|
712
|
+
|
|
713
|
+
```dart
|
|
714
|
+
// BAD: Context used after async gap
|
|
715
|
+
Future<void> _submit() async {
|
|
716
|
+
final response = await api.submitForm(data);
|
|
717
|
+
|
|
718
|
+
// Widget might be disposed by now!
|
|
719
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
720
|
+
SnackBar(content: Text('Success!')),
|
|
721
|
+
);
|
|
722
|
+
Navigator.of(context).pushReplacementNamed('/home');
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**Why developers do it:**
|
|
727
|
+
|
|
728
|
+
It works most of the time because the widget is usually still mounted when the `Future` completes. The bug only surfaces when users navigate away during the async operation (press back while a form is submitting), which is a race condition that rarely occurs during development but happens constantly in production.
|
|
729
|
+
|
|
730
|
+
**What goes wrong:**
|
|
731
|
+
|
|
732
|
+
- `BuildContext` references a widget that no longer exists in the tree, leading to "Looking up a deactivated widget's ancestor is unsafe" errors.
|
|
733
|
+
- On navigation-heavy apps, this causes crashes that are difficult to reproduce because they depend on timing.
|
|
734
|
+
- The Dart linter rule `use_build_context_synchronously` was created specifically because this pattern caused so many production crashes.
|
|
735
|
+
- Flutter GitHub issues #110694 and #122953 document extensive community frustration with this pattern.
|
|
736
|
+
|
|
737
|
+
**The fix:**
|
|
738
|
+
|
|
739
|
+
```dart
|
|
740
|
+
// GOOD: Check mounted before using context after await
|
|
741
|
+
Future<void> _submit() async {
|
|
742
|
+
final response = await api.submitForm(data);
|
|
743
|
+
|
|
744
|
+
if (!mounted) return; // Widget was disposed during await
|
|
745
|
+
|
|
746
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
747
|
+
SnackBar(content: Text('Success!')),
|
|
748
|
+
);
|
|
749
|
+
Navigator.of(context).pushReplacementNamed('/home');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// BETTER: Use a callback pattern or state management
|
|
753
|
+
// so the widget doesn't need context after the async gap
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**Detection rule:**
|
|
757
|
+
If any method calls `await` and then references `context` (including `Navigator.of(context)`, `ScaffoldMessenger.of(context)`, `Theme.of(context)`, `MediaQuery.of(context)`) without a `mounted` check between the `await` and the context usage, it is AP-09. Lint rule: `use_build_context_synchronously`.
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
### AP-10: FutureBuilder/StreamBuilder Without Handling All States
|
|
762
|
+
|
|
763
|
+
**Also known as:** The Missing Loading Screen, Partial Snapshot Handling, Optimistic Builder
|
|
764
|
+
**Frequency:** Very Common
|
|
765
|
+
**Severity:** Medium
|
|
766
|
+
**Detection difficulty:** Easy
|
|
767
|
+
|
|
768
|
+
**What it looks like:**
|
|
769
|
+
|
|
770
|
+
Using `FutureBuilder` or `StreamBuilder` but only checking for the "done with data" case, ignoring loading, error, and null-data states.
|
|
771
|
+
|
|
772
|
+
```dart
|
|
773
|
+
// BAD: Only handles success case
|
|
774
|
+
FutureBuilder<User>(
|
|
775
|
+
future: fetchUser(),
|
|
776
|
+
builder: (context, snapshot) {
|
|
777
|
+
// What about loading? Errors? Null data?
|
|
778
|
+
return Text(snapshot.data!.name); // Crashes if data is null
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**Why developers do it:**
|
|
784
|
+
|
|
785
|
+
During development, the API is fast and always succeeds. Developers focus on the happy path because that is what they see in the emulator. The `snapshot.data!` force-unwrap suppresses the null warning, making the code "work" in dev.
|
|
786
|
+
|
|
787
|
+
**What goes wrong:**
|
|
788
|
+
|
|
789
|
+
- `snapshot.data` is null during `ConnectionState.waiting`, causing null-pointer crashes on slow networks.
|
|
790
|
+
- API errors produce `snapshot.hasError == true` but no error UI is shown; users see a blank screen or crash.
|
|
791
|
+
- On rebuilds, the `FutureBuilder` briefly returns to `ConnectionState.waiting` before the data arrives again, causing screen flicker.
|
|
792
|
+
- A common secondary bug: creating the Future inside `build()` causes the request to fire on every rebuild (see AP-14).
|
|
793
|
+
|
|
794
|
+
**The fix:**
|
|
795
|
+
|
|
796
|
+
```dart
|
|
797
|
+
// GOOD: Handle all states
|
|
798
|
+
FutureBuilder<User>(
|
|
799
|
+
future: _userFuture, // Created in initState, not build!
|
|
800
|
+
builder: (context, snapshot) {
|
|
801
|
+
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
802
|
+
return const Center(child: CircularProgressIndicator());
|
|
803
|
+
}
|
|
804
|
+
if (snapshot.hasError) {
|
|
805
|
+
return Center(child: Text('Error: ${snapshot.error}'));
|
|
806
|
+
}
|
|
807
|
+
if (!snapshot.hasData) {
|
|
808
|
+
return const Center(child: Text('No user found'));
|
|
809
|
+
}
|
|
810
|
+
final user = snapshot.data!;
|
|
811
|
+
return Text(user.name);
|
|
812
|
+
},
|
|
813
|
+
);
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Detection rule:**
|
|
817
|
+
If a `FutureBuilder` or `StreamBuilder` builder function does not contain checks for `ConnectionState.waiting`, `snapshot.hasError`, and `snapshot.hasData`, it is AP-10. If `snapshot.data!` is used without a preceding null/error check, it is AP-10.
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
### AP-11: Hardcoded Dimensions
|
|
822
|
+
|
|
823
|
+
**Also known as:** Pixel-Perfect Fragility, The Single-Device Design, Magic Numbers
|
|
824
|
+
**Frequency:** Very Common
|
|
825
|
+
**Severity:** Medium
|
|
826
|
+
**Detection difficulty:** Easy
|
|
827
|
+
|
|
828
|
+
**What it looks like:**
|
|
829
|
+
|
|
830
|
+
Using fixed pixel values for widths, heights, padding, and font sizes instead of responsive sizing.
|
|
831
|
+
|
|
832
|
+
```dart
|
|
833
|
+
// BAD: Hardcoded dimensions
|
|
834
|
+
Container(
|
|
835
|
+
width: 375, // iPhone 8 width
|
|
836
|
+
height: 812, // iPhone X height
|
|
837
|
+
padding: EdgeInsets.only(top: 44), // iPhone X safe area, hardcoded
|
|
838
|
+
child: Column(
|
|
839
|
+
children: [
|
|
840
|
+
SizedBox(height: 200), // Fixed header height
|
|
841
|
+
Container(
|
|
842
|
+
width: 340,
|
|
843
|
+
height: 50,
|
|
844
|
+
child: TextField(),
|
|
845
|
+
),
|
|
846
|
+
],
|
|
847
|
+
),
|
|
848
|
+
);
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
**Why developers do it:**
|
|
852
|
+
|
|
853
|
+
The app looks perfect on the developer's test device. Flutter's hot reload makes it easy to tweak pixels until it looks right on one screen. Designers hand off specs in exact pixel values. Using `MediaQuery` or `LayoutBuilder` requires understanding constraints, which adds complexity.
|
|
854
|
+
|
|
855
|
+
**What goes wrong:**
|
|
856
|
+
|
|
857
|
+
- Overflow errors on smaller screens (the yellow-and-black striped bar that every Flutter developer recognizes).
|
|
858
|
+
- Wasted space on tablets and foldables.
|
|
859
|
+
- Text truncation on devices with larger system font sizes.
|
|
860
|
+
- Landscape mode becomes completely broken.
|
|
861
|
+
- Apps that look fine on a Pixel 6 break on Samsung Galaxy Fold, iPad, or budget Android devices with unusual aspect ratios.
|
|
862
|
+
|
|
863
|
+
**The fix:**
|
|
864
|
+
|
|
865
|
+
```dart
|
|
866
|
+
// GOOD: Responsive dimensions
|
|
867
|
+
LayoutBuilder(
|
|
868
|
+
builder: (context, constraints) {
|
|
869
|
+
return Padding(
|
|
870
|
+
padding: EdgeInsets.symmetric(
|
|
871
|
+
horizontal: constraints.maxWidth * 0.05,
|
|
872
|
+
),
|
|
873
|
+
child: Column(
|
|
874
|
+
children: [
|
|
875
|
+
SizedBox(height: constraints.maxHeight * 0.1),
|
|
876
|
+
ConstrainedBox(
|
|
877
|
+
constraints: BoxConstraints(maxWidth: 600),
|
|
878
|
+
child: const TextField(),
|
|
879
|
+
),
|
|
880
|
+
],
|
|
881
|
+
),
|
|
882
|
+
);
|
|
883
|
+
},
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
// Also: use MediaQuery.paddingOf(context) for safe area
|
|
887
|
+
// Also: use Flexible/Expanded instead of fixed SizedBox
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
**Detection rule:**
|
|
891
|
+
If a `Container`, `SizedBox`, `Padding`, or `Positioned` uses literal numeric values greater than 100 for width/height, suspect AP-11. If `EdgeInsets` uses literal values greater than 32, verify they are intentional. Exception: small spacing values (4, 8, 16) are usually fine.
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
### AP-12: Rebuilding Entire Widget Trees Unnecessarily
|
|
896
|
+
|
|
897
|
+
**Also known as:** The Rebuild Avalanche, Unscoped State, Overly Broad setState
|
|
898
|
+
**Frequency:** Very Common
|
|
899
|
+
**Severity:** High
|
|
900
|
+
**Detection difficulty:** Hard
|
|
901
|
+
|
|
902
|
+
**What it looks like:**
|
|
903
|
+
|
|
904
|
+
A `setState` call or state change triggers a rebuild of the entire screen when only a small portion of the UI actually changed.
|
|
905
|
+
|
|
906
|
+
```dart
|
|
907
|
+
// BAD: Entire screen rebuilds when only the counter changes
|
|
908
|
+
class DashboardScreen extends StatefulWidget {
|
|
909
|
+
@override
|
|
910
|
+
_DashboardScreenState createState() => _DashboardScreenState();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
class _DashboardScreenState extends State<DashboardScreen> {
|
|
914
|
+
int notificationCount = 0;
|
|
915
|
+
|
|
916
|
+
@override
|
|
917
|
+
Widget build(BuildContext context) {
|
|
918
|
+
return Scaffold(
|
|
919
|
+
appBar: AppBar(
|
|
920
|
+
title: Text('Dashboard'),
|
|
921
|
+
actions: [
|
|
922
|
+
Badge(
|
|
923
|
+
label: Text('$notificationCount'), // Only this changes
|
|
924
|
+
child: Icon(Icons.notifications),
|
|
925
|
+
),
|
|
926
|
+
],
|
|
927
|
+
),
|
|
928
|
+
body: Column(
|
|
929
|
+
children: [
|
|
930
|
+
ExpensiveChart(), // Rebuilds unnecessarily
|
|
931
|
+
ExpensiveUserList(), // Rebuilds unnecessarily
|
|
932
|
+
ExpensiveActivityFeed(),// Rebuilds unnecessarily
|
|
933
|
+
],
|
|
934
|
+
),
|
|
935
|
+
floatingActionButton: FloatingActionButton(
|
|
936
|
+
onPressed: () => setState(() => notificationCount++),
|
|
937
|
+
),
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
**Why developers do it:**
|
|
944
|
+
|
|
945
|
+
`setState` is a blunt instrument -- it marks the entire `State` as dirty. Developers do not realize that every child widget's `build()` runs again. Flutter DevTools is not always used during development, so the rebuild count goes unnoticed.
|
|
946
|
+
|
|
947
|
+
**What goes wrong:**
|
|
948
|
+
|
|
949
|
+
- In the example above, every tap rebuilds `ExpensiveChart`, `ExpensiveUserList`, and `ExpensiveActivityFeed` even though their data has not changed.
|
|
950
|
+
- On screens with heavy widgets (charts, maps, video players), this causes visible stutter.
|
|
951
|
+
- Community reports document apps dropping from 60fps to 15fps due to a single poorly scoped `setState` on a complex dashboard.
|
|
952
|
+
- Calling `MediaQuery.of(context)` in the root widget creates an implicit dependency that rebuilds the entire tree when the keyboard opens or orientation changes.
|
|
953
|
+
|
|
954
|
+
**The fix:**
|
|
955
|
+
|
|
956
|
+
Push state down to the smallest widget that needs it, or use granular state management.
|
|
957
|
+
|
|
958
|
+
```dart
|
|
959
|
+
// GOOD: Only the badge rebuilds
|
|
960
|
+
class NotificationBadge extends StatelessWidget {
|
|
961
|
+
const NotificationBadge();
|
|
962
|
+
|
|
963
|
+
@override
|
|
964
|
+
Widget build(BuildContext context) {
|
|
965
|
+
final count = context.watch<NotificationProvider>().count;
|
|
966
|
+
return Badge(
|
|
967
|
+
label: Text('$count'),
|
|
968
|
+
child: const Icon(Icons.notifications),
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Dashboard no longer rebuilds its expensive children
|
|
974
|
+
class DashboardScreen extends StatelessWidget {
|
|
975
|
+
const DashboardScreen();
|
|
976
|
+
|
|
977
|
+
@override
|
|
978
|
+
Widget build(BuildContext context) {
|
|
979
|
+
return Scaffold(
|
|
980
|
+
appBar: AppBar(
|
|
981
|
+
title: const Text('Dashboard'),
|
|
982
|
+
actions: [const NotificationBadge()],
|
|
983
|
+
),
|
|
984
|
+
body: const Column(
|
|
985
|
+
children: [
|
|
986
|
+
ExpensiveChart(),
|
|
987
|
+
ExpensiveUserList(),
|
|
988
|
+
ExpensiveActivityFeed(),
|
|
989
|
+
],
|
|
990
|
+
),
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
**Detection rule:**
|
|
997
|
+
If `setState` modifies only one variable but the `build()` method constructs 5+ widgets, suspect AP-12. Use Flutter DevTools Rebuild Stats to verify: if widgets with unchanged data show rebuild counts >1, it is AP-12.
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
### AP-13: Mixing Business Logic in Widgets
|
|
1002
|
+
|
|
1003
|
+
**Also known as:** Fat Widget, Logic Leak, The Untestable Screen
|
|
1004
|
+
**Frequency:** Very Common
|
|
1005
|
+
**Severity:** High
|
|
1006
|
+
**Detection difficulty:** Moderate
|
|
1007
|
+
|
|
1008
|
+
**What it looks like:**
|
|
1009
|
+
|
|
1010
|
+
API calls, data transformation, validation logic, and business rules living directly inside widget classes.
|
|
1011
|
+
|
|
1012
|
+
```dart
|
|
1013
|
+
// BAD: Business logic embedded in widget
|
|
1014
|
+
class OrderScreen extends StatefulWidget {
|
|
1015
|
+
@override
|
|
1016
|
+
_OrderScreenState createState() => _OrderScreenState();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
class _OrderScreenState extends State<OrderScreen> {
|
|
1020
|
+
Future<void> placeOrder() async {
|
|
1021
|
+
// Validation logic in widget
|
|
1022
|
+
if (cart.isEmpty) { showError('Cart is empty'); return; }
|
|
1023
|
+
if (cart.total < 10.0) { showError('Minimum order: \$10'); return; }
|
|
1024
|
+
|
|
1025
|
+
// Price calculation in widget
|
|
1026
|
+
final subtotal = cart.items.fold(0.0, (sum, i) => sum + i.price * i.qty);
|
|
1027
|
+
final tax = subtotal * 0.08;
|
|
1028
|
+
final shipping = subtotal > 50 ? 0 : 5.99;
|
|
1029
|
+
final total = subtotal + tax + shipping;
|
|
1030
|
+
|
|
1031
|
+
// API call in widget
|
|
1032
|
+
final response = await http.post(
|
|
1033
|
+
Uri.parse('https://api.example.com/orders'),
|
|
1034
|
+
body: jsonEncode({'items': cart.items, 'total': total}),
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
// Navigation in response to API
|
|
1038
|
+
if (response.statusCode == 201) {
|
|
1039
|
+
Navigator.pushNamed(context, '/confirmation');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
@override
|
|
1044
|
+
Widget build(BuildContext context) => /* ... */;
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
**Why developers do it:**
|
|
1049
|
+
|
|
1050
|
+
It is the fastest way to get features working. During prototyping, separating layers feels like unnecessary abstraction. Small apps genuinely do not need separation. The problem is that developers never go back to refactor once the app grows.
|
|
1051
|
+
|
|
1052
|
+
**What goes wrong:**
|
|
1053
|
+
|
|
1054
|
+
- Business rules (minimum order amount, tax calculation, shipping thresholds) cannot be unit tested without instantiating a widget.
|
|
1055
|
+
- Changing the API endpoint requires editing a UI file.
|
|
1056
|
+
- The same business logic gets duplicated across multiple screens (order confirmation, order history, admin dashboard all recalculate totals).
|
|
1057
|
+
- When business rules change (tax rate update), developers must find and update every widget that embedded the old rate.
|
|
1058
|
+
- Flutter's official architecture guide explicitly warns against this: "Views should not contain any business logic."
|
|
1059
|
+
|
|
1060
|
+
**The fix:**
|
|
1061
|
+
|
|
1062
|
+
```dart
|
|
1063
|
+
// GOOD: Separated layers
|
|
1064
|
+
// domain/order_service.dart
|
|
1065
|
+
class OrderService {
|
|
1066
|
+
final ApiClient _api;
|
|
1067
|
+
OrderService(this._api);
|
|
1068
|
+
|
|
1069
|
+
double calculateTotal(Cart cart) {
|
|
1070
|
+
final subtotal = cart.items.fold(0.0, (sum, i) => sum + i.price * i.qty);
|
|
1071
|
+
final tax = subtotal * 0.08;
|
|
1072
|
+
final shipping = subtotal > 50 ? 0.0 : 5.99;
|
|
1073
|
+
return subtotal + tax + shipping;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
String? validate(Cart cart) {
|
|
1077
|
+
if (cart.isEmpty) return 'Cart is empty';
|
|
1078
|
+
if (cart.total < 10.0) return 'Minimum order: \$10';
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
Future<Order> placeOrder(Cart cart) async {
|
|
1083
|
+
final total = calculateTotal(cart);
|
|
1084
|
+
return _api.post('/orders', {'items': cart.items, 'total': total});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Widget is now thin -- only UI concerns
|
|
1089
|
+
class OrderScreen extends ConsumerWidget {
|
|
1090
|
+
@override
|
|
1091
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
1092
|
+
return ElevatedButton(
|
|
1093
|
+
onPressed: () async {
|
|
1094
|
+
final service = ref.read(orderServiceProvider);
|
|
1095
|
+
final error = service.validate(cart);
|
|
1096
|
+
if (error != null) { showError(error); return; }
|
|
1097
|
+
await service.placeOrder(cart);
|
|
1098
|
+
if (context.mounted) Navigator.pushNamed(context, '/confirmation');
|
|
1099
|
+
},
|
|
1100
|
+
child: const Text('Place Order'),
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
**Detection rule:**
|
|
1107
|
+
If a widget class imports `dart:convert`, `package:http`, or a database package, suspect AP-13. If a widget method contains conditional business logic (price calculation, validation rules, permission checks), it is AP-13.
|
|
1108
|
+
|
|
1109
|
+
---
|
|
1110
|
+
|
|
1111
|
+
### AP-14: Creating Futures/Streams Inside build()
|
|
1112
|
+
|
|
1113
|
+
**Also known as:** The Infinite Request Loop, Build-Time Side Effects
|
|
1114
|
+
**Frequency:** Common
|
|
1115
|
+
**Severity:** Critical
|
|
1116
|
+
**Detection difficulty:** Moderate
|
|
1117
|
+
|
|
1118
|
+
**What it looks like:**
|
|
1119
|
+
|
|
1120
|
+
Calling an API, creating a `Future`, or opening a `Stream` inside the `build()` method, often inside a `FutureBuilder` or `StreamBuilder`.
|
|
1121
|
+
|
|
1122
|
+
```dart
|
|
1123
|
+
// BAD: API called on every rebuild
|
|
1124
|
+
class UserProfile extends StatelessWidget {
|
|
1125
|
+
@override
|
|
1126
|
+
Widget build(BuildContext context) {
|
|
1127
|
+
return FutureBuilder<User>(
|
|
1128
|
+
future: fetchUser(), // Called on EVERY rebuild!
|
|
1129
|
+
builder: (context, snapshot) {
|
|
1130
|
+
if (snapshot.hasData) return Text(snapshot.data!.name);
|
|
1131
|
+
return CircularProgressIndicator();
|
|
1132
|
+
},
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
**Why developers do it:**
|
|
1139
|
+
|
|
1140
|
+
`FutureBuilder` accepts a `future` parameter, so developers naturally pass the function call directly. It works on the first render. The problem only appears when the widget rebuilds (parent setState, keyboard open/close, navigation) and fires the request again.
|
|
1141
|
+
|
|
1142
|
+
**What goes wrong:**
|
|
1143
|
+
|
|
1144
|
+
- Every rebuild triggers a new network request, potentially hundreds per second during animations or scrolling.
|
|
1145
|
+
- The UI flickers between loading and loaded states as each new request starts.
|
|
1146
|
+
- API rate limits are hit. Server costs spike.
|
|
1147
|
+
- The Flutter `FutureBuilder` class documentation explicitly warns: "The future must not be created during the State.build or StatelessWidget.build method call."
|
|
1148
|
+
|
|
1149
|
+
**The fix:**
|
|
1150
|
+
|
|
1151
|
+
```dart
|
|
1152
|
+
// GOOD: Future created once in initState
|
|
1153
|
+
class UserProfile extends StatefulWidget {
|
|
1154
|
+
@override
|
|
1155
|
+
_UserProfileState createState() => _UserProfileState();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
class _UserProfileState extends State<UserProfile> {
|
|
1159
|
+
late final Future<User> _userFuture;
|
|
1160
|
+
|
|
1161
|
+
@override
|
|
1162
|
+
void initState() {
|
|
1163
|
+
super.initState();
|
|
1164
|
+
_userFuture = fetchUser(); // Created once
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
@override
|
|
1168
|
+
Widget build(BuildContext context) {
|
|
1169
|
+
return FutureBuilder<User>(
|
|
1170
|
+
future: _userFuture, // Same Future instance on every rebuild
|
|
1171
|
+
builder: (context, snapshot) {
|
|
1172
|
+
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
1173
|
+
return const CircularProgressIndicator();
|
|
1174
|
+
}
|
|
1175
|
+
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
|
|
1176
|
+
return Text(snapshot.data!.name);
|
|
1177
|
+
},
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
**Detection rule:**
|
|
1184
|
+
If a `FutureBuilder` or `StreamBuilder` has its `future:` or `stream:` parameter set to a function call (not a variable reference), it is AP-14. Pattern: `FutureBuilder(future: someFunctionCall(),` inside a `build()` method.
|
|
1185
|
+
|
|
1186
|
+
---
|
|
1187
|
+
|
|
1188
|
+
### AP-15: Overusing StatefulWidget
|
|
1189
|
+
|
|
1190
|
+
**Also known as:** Stateful by Default, The Unnecessary Lifecycle
|
|
1191
|
+
**Frequency:** Common
|
|
1192
|
+
**Severity:** Low
|
|
1193
|
+
**Detection difficulty:** Easy
|
|
1194
|
+
|
|
1195
|
+
**What it looks like:**
|
|
1196
|
+
|
|
1197
|
+
Using `StatefulWidget` for widgets that have no mutable state, or that receive all their data from parent widgets or state management.
|
|
1198
|
+
|
|
1199
|
+
```dart
|
|
1200
|
+
// BAD: StatefulWidget with no mutable state
|
|
1201
|
+
class UserCard extends StatefulWidget {
|
|
1202
|
+
final User user;
|
|
1203
|
+
const UserCard({required this.user});
|
|
1204
|
+
|
|
1205
|
+
@override
|
|
1206
|
+
_UserCardState createState() => _UserCardState();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
class _UserCardState extends State<UserCard> {
|
|
1210
|
+
// No state variables!
|
|
1211
|
+
// No initState doing anything!
|
|
1212
|
+
// No dispose needed!
|
|
1213
|
+
|
|
1214
|
+
@override
|
|
1215
|
+
Widget build(BuildContext context) {
|
|
1216
|
+
return Card(
|
|
1217
|
+
child: ListTile(
|
|
1218
|
+
title: Text(widget.user.name),
|
|
1219
|
+
subtitle: Text(widget.user.email),
|
|
1220
|
+
),
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
**Why developers do it:**
|
|
1227
|
+
|
|
1228
|
+
Beginners are unsure whether they will need state later, so they default to `StatefulWidget` "just in case." Some tutorials use `StatefulWidget` for all examples. IDE templates sometimes generate `StatefulWidget` scaffolds.
|
|
1229
|
+
|
|
1230
|
+
**What goes wrong:**
|
|
1231
|
+
|
|
1232
|
+
- `StatefulWidget` allocates a `State` object with its own lifecycle, consuming more memory than `StatelessWidget`.
|
|
1233
|
+
- It signals to other developers that this widget has mutable state, misleading code reviewers.
|
|
1234
|
+
- It prevents the widget from being `const`, eliminating Flutter's rebuild optimization.
|
|
1235
|
+
- While the performance impact per widget is small, in a list of hundreds of items (e.g., a chat app), the cumulative overhead is measurable.
|
|
1236
|
+
|
|
1237
|
+
**The fix:**
|
|
1238
|
+
|
|
1239
|
+
```dart
|
|
1240
|
+
// GOOD: StatelessWidget when there's no state
|
|
1241
|
+
class UserCard extends StatelessWidget {
|
|
1242
|
+
final User user;
|
|
1243
|
+
const UserCard({required this.user});
|
|
1244
|
+
|
|
1245
|
+
@override
|
|
1246
|
+
Widget build(BuildContext context) {
|
|
1247
|
+
return Card(
|
|
1248
|
+
child: ListTile(
|
|
1249
|
+
title: Text(user.name),
|
|
1250
|
+
subtitle: Text(user.email),
|
|
1251
|
+
),
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
**Detection rule:**
|
|
1258
|
+
If a `State` class has no mutable fields (no `late` variables, no non-final fields), no `initState` override, and no `dispose` override, the widget should be `StatelessWidget`. This is AP-15.
|
|
1259
|
+
|
|
1260
|
+
---
|
|
1261
|
+
|
|
1262
|
+
### AP-16: Abusing the Null Check Operator (!)
|
|
1263
|
+
|
|
1264
|
+
**Also known as:** Bang Operator Abuse, Force Unwrap Everywhere, The Lazy Null Fix
|
|
1265
|
+
**Frequency:** Very Common
|
|
1266
|
+
**Severity:** High
|
|
1267
|
+
**Detection difficulty:** Easy
|
|
1268
|
+
|
|
1269
|
+
**What it looks like:**
|
|
1270
|
+
|
|
1271
|
+
Using the `!` operator to silence null safety warnings instead of handling nullable types properly.
|
|
1272
|
+
|
|
1273
|
+
```dart
|
|
1274
|
+
// BAD: Force unwrapping everywhere
|
|
1275
|
+
class UserProfile extends StatelessWidget {
|
|
1276
|
+
final User? user;
|
|
1277
|
+
const UserProfile({this.user});
|
|
1278
|
+
|
|
1279
|
+
@override
|
|
1280
|
+
Widget build(BuildContext context) {
|
|
1281
|
+
return Column(
|
|
1282
|
+
children: [
|
|
1283
|
+
Text(user!.name), // Crashes if user is null
|
|
1284
|
+
Text(user!.email!), // Double crash potential
|
|
1285
|
+
Text(user!.address!.city!), // Triple crash potential
|
|
1286
|
+
Image.network(user!.avatar!),
|
|
1287
|
+
],
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// BAD: Using ! to "fix" migration warnings
|
|
1293
|
+
String getUserName(Map<String, dynamic> json) {
|
|
1294
|
+
return json['name']! as String; // Crashes on missing key
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
**Why developers do it:**
|
|
1299
|
+
|
|
1300
|
+
The `!` operator is the fastest way to make a null-safety error disappear. During the null safety migration, the `dart migrate` tool sometimes inserted `!` in places where proper null handling was needed. Developers see `!` used in official examples (e.g., `snapshot.data!` in FutureBuilder) and generalize its use.
|
|
1301
|
+
|
|
1302
|
+
**What goes wrong:**
|
|
1303
|
+
|
|
1304
|
+
- Every `!` is an implicit assertion that throws a runtime exception if the value is null, converting a compile-time safety feature into a runtime crash.
|
|
1305
|
+
- In production, this manifests as `Null check operator used on a null value` -- one of the most common Flutter crash reports.
|
|
1306
|
+
- The crash message gives no context about which `!` caused it when multiple are on the same line.
|
|
1307
|
+
- It defeats the entire purpose of Dart's sound null safety, which was designed to eliminate null pointer exceptions at compile time.
|
|
1308
|
+
|
|
1309
|
+
**The fix:**
|
|
1310
|
+
|
|
1311
|
+
```dart
|
|
1312
|
+
// GOOD: Proper null handling
|
|
1313
|
+
class UserProfile extends StatelessWidget {
|
|
1314
|
+
final User? user;
|
|
1315
|
+
const UserProfile({this.user});
|
|
1316
|
+
|
|
1317
|
+
@override
|
|
1318
|
+
Widget build(BuildContext context) {
|
|
1319
|
+
final currentUser = user;
|
|
1320
|
+
if (currentUser == null) {
|
|
1321
|
+
return const Center(child: Text('No user data'));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return Column(
|
|
1325
|
+
children: [
|
|
1326
|
+
Text(currentUser.name),
|
|
1327
|
+
Text(currentUser.email ?? 'No email'),
|
|
1328
|
+
Text(currentUser.address?.city ?? 'Unknown city'),
|
|
1329
|
+
if (currentUser.avatar != null)
|
|
1330
|
+
Image.network(currentUser.avatar!),
|
|
1331
|
+
],
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
**Detection rule:**
|
|
1338
|
+
If `!` appears more than 3 times in a single method, suspect AP-16. If `!` is used on a value that could legitimately be null at runtime (user input, API response, map lookup), it is AP-16. Pattern: `json['key']!`, `snapshot.data!` without preceding `hasData` check, chained `!` operators like `a!.b!.c!`.
|
|
1339
|
+
|
|
1340
|
+
---
|
|
1341
|
+
|
|
1342
|
+
### AP-17: Not Handling Platform Differences
|
|
1343
|
+
|
|
1344
|
+
**Also known as:** iOS-Only Development, Android Amnesia, The Platform Blindspot
|
|
1345
|
+
**Frequency:** Common
|
|
1346
|
+
**Severity:** Medium
|
|
1347
|
+
**Detection difficulty:** Hard
|
|
1348
|
+
|
|
1349
|
+
**What it looks like:**
|
|
1350
|
+
|
|
1351
|
+
Assuming Flutter automatically handles all platform differences, ignoring iOS/Android behavioral differences, safe areas, navigation patterns, and permissions.
|
|
1352
|
+
|
|
1353
|
+
```dart
|
|
1354
|
+
// BAD: Ignoring platform differences
|
|
1355
|
+
class SettingsScreen extends StatelessWidget {
|
|
1356
|
+
@override
|
|
1357
|
+
Widget build(BuildContext context) {
|
|
1358
|
+
return Scaffold(
|
|
1359
|
+
appBar: AppBar(title: Text('Settings')),
|
|
1360
|
+
body: ListView(
|
|
1361
|
+
children: [
|
|
1362
|
+
ListTile(
|
|
1363
|
+
title: Text('Notifications'),
|
|
1364
|
+
onTap: () {
|
|
1365
|
+
// Only handles Android permission model
|
|
1366
|
+
requestNotificationPermission();
|
|
1367
|
+
},
|
|
1368
|
+
),
|
|
1369
|
+
ListTile(
|
|
1370
|
+
title: Text('Share'),
|
|
1371
|
+
onTap: () {
|
|
1372
|
+
// Uses Android intent pattern, breaks on iOS
|
|
1373
|
+
shareViaIntent(data);
|
|
1374
|
+
},
|
|
1375
|
+
),
|
|
1376
|
+
],
|
|
1377
|
+
),
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
**Why developers do it:**
|
|
1384
|
+
|
|
1385
|
+
Flutter's "write once, run anywhere" marketing creates a false sense of complete platform abstraction. Developers test on only one platform (usually the one their primary device runs). Platform-specific behavior is documented across multiple Flutter and platform-specific docs, making it easy to miss.
|
|
1386
|
+
|
|
1387
|
+
**What goes wrong:**
|
|
1388
|
+
|
|
1389
|
+
- iOS back navigation (swipe-from-edge) conflicts with horizontal scroll/drawer gestures if not handled.
|
|
1390
|
+
- Android 13+ requires explicit notification permission requests; iOS has required this since iOS 10.
|
|
1391
|
+
- `WillPopScope` (now `PopScope`) behaves differently with Android predictive back gestures (Android 13+), as documented in Flutter breaking changes and GitHub issue #140869.
|
|
1392
|
+
- Status bar and notch/Dynamic Island handling differs between platforms.
|
|
1393
|
+
- File paths, app lifecycle events, and deep linking all have platform-specific behaviors.
|
|
1394
|
+
|
|
1395
|
+
**The fix:**
|
|
1396
|
+
|
|
1397
|
+
```dart
|
|
1398
|
+
// GOOD: Platform-aware code
|
|
1399
|
+
import 'dart:io' show Platform;
|
|
1400
|
+
|
|
1401
|
+
class SettingsScreen extends StatelessWidget {
|
|
1402
|
+
@override
|
|
1403
|
+
Widget build(BuildContext context) {
|
|
1404
|
+
return Scaffold(
|
|
1405
|
+
appBar: AppBar(title: const Text('Settings')),
|
|
1406
|
+
body: SafeArea( // Handles notch, Dynamic Island, etc.
|
|
1407
|
+
child: ListView(
|
|
1408
|
+
children: [
|
|
1409
|
+
ListTile(
|
|
1410
|
+
title: const Text('Notifications'),
|
|
1411
|
+
onTap: () async {
|
|
1412
|
+
if (Platform.isIOS) {
|
|
1413
|
+
await requestIOSNotificationPermission();
|
|
1414
|
+
} else {
|
|
1415
|
+
await requestAndroidNotificationPermission();
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
),
|
|
1419
|
+
],
|
|
1420
|
+
),
|
|
1421
|
+
),
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
**Detection rule:**
|
|
1428
|
+
If a Flutter app targets both iOS and Android but contains no `Platform.isIOS` / `Platform.isAndroid` checks and no `SafeArea` widgets, suspect AP-17. If `PopScope` or navigation handling does not account for Android predictive back, it is AP-17.
|
|
1429
|
+
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
### AP-18: Over-Relying on Packages for Simple Tasks
|
|
1433
|
+
|
|
1434
|
+
**Also known as:** Package Bloat, The pub.dev Reflex, Dependency Addiction
|
|
1435
|
+
**Frequency:** Common
|
|
1436
|
+
**Severity:** Medium
|
|
1437
|
+
**Detection difficulty:** Moderate
|
|
1438
|
+
|
|
1439
|
+
**What it looks like:**
|
|
1440
|
+
|
|
1441
|
+
Adding third-party packages for functionality that Dart or Flutter provide natively, or for trivial implementations.
|
|
1442
|
+
|
|
1443
|
+
```dart
|
|
1444
|
+
# pubspec.yaml -- BAD: packages for trivial tasks
|
|
1445
|
+
dependencies:
|
|
1446
|
+
flutter:
|
|
1447
|
+
sdk: flutter
|
|
1448
|
+
string_extensions: ^1.0.0 # For .capitalize() -- just write it
|
|
1449
|
+
date_formatter: ^2.0.0 # For date formatting -- intl is built-in
|
|
1450
|
+
device_info: ^3.0.0 # Used once to get platform name
|
|
1451
|
+
url_launcher: ^6.0.0 # Actually needed
|
|
1452
|
+
connectivity: ^3.0.0 # Actually needed
|
|
1453
|
+
random_color: ^1.0.0 # For generating random colors -- 3 lines of code
|
|
1454
|
+
validators: ^2.0.0 # For email validation -- one regex
|
|
1455
|
+
screen_size_util: ^1.0.0 # For screen breakpoints -- 10 lines of code
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
**Why developers do it:**
|
|
1459
|
+
|
|
1460
|
+
pub.dev makes it trivially easy to add packages. "There's a package for that" is a common response in the Flutter community. Developers overestimate the complexity of implementing simple utilities. Adding a package feels faster than writing 10 lines of code.
|
|
1461
|
+
|
|
1462
|
+
**What goes wrong:**
|
|
1463
|
+
|
|
1464
|
+
- Each package adds transitive dependencies, inflating binary size.
|
|
1465
|
+
- Version conflicts between packages become frequent -- `flutter pub get` starts failing with "version solving failed" as documented in FlutterFlow community posts and Dart dependency management guides.
|
|
1466
|
+
- Abandoned or unmaintained packages create security vulnerabilities and compatibility issues with new Flutter versions.
|
|
1467
|
+
- Upgrading Flutter SDK often breaks multiple packages simultaneously.
|
|
1468
|
+
- Debug builds become slow because Dart analyzes all dependency code.
|
|
1469
|
+
- One popular left-pad-style incident: when a package author unpublished their package, multiple apps broke.
|
|
1470
|
+
|
|
1471
|
+
**The fix:**
|
|
1472
|
+
|
|
1473
|
+
Before adding a package, ask: "Can I implement this in under 20 lines?" If yes, write it yourself.
|
|
1474
|
+
|
|
1475
|
+
```dart
|
|
1476
|
+
// Instead of string_extensions package:
|
|
1477
|
+
extension StringCaps on String {
|
|
1478
|
+
String get capitalize =>
|
|
1479
|
+
isEmpty ? '' : '${this[0].toUpperCase()}${substring(1)}';
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// Instead of random_color package:
|
|
1483
|
+
Color randomColor() => Color((Random().nextDouble() * 0xFFFFFF).toInt())
|
|
1484
|
+
.withOpacity(1.0);
|
|
1485
|
+
|
|
1486
|
+
// Instead of validators package for email:
|
|
1487
|
+
bool isValidEmail(String email) =>
|
|
1488
|
+
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
**Detection rule:**
|
|
1492
|
+
If `pubspec.yaml` has more than 15 direct dependencies, review each for necessity. If a package is imported in only one file and used for a single function call, suspect AP-18. Run `dart pub deps --no-dev` and check for deep transitive dependency trees.
|
|
1493
|
+
|
|
1494
|
+
---
|
|
1495
|
+
|
|
1496
|
+
### AP-19: Not Handling Navigation and Back Button Properly
|
|
1497
|
+
|
|
1498
|
+
**Also known as:** Navigation Amnesia, Double-Pop Bug, Exit-on-Accident
|
|
1499
|
+
**Frequency:** Common
|
|
1500
|
+
**Severity:** Medium
|
|
1501
|
+
**Detection difficulty:** Moderate
|
|
1502
|
+
|
|
1503
|
+
**What it looks like:**
|
|
1504
|
+
|
|
1505
|
+
Not intercepting the back button on critical screens (forms, payment flows), allowing accidental data loss. Not handling deep link navigation state. Using deprecated `WillPopScope` on Flutter 3.12+.
|
|
1506
|
+
|
|
1507
|
+
```dart
|
|
1508
|
+
// BAD: No back button protection on a checkout flow
|
|
1509
|
+
class CheckoutScreen extends StatefulWidget {
|
|
1510
|
+
@override
|
|
1511
|
+
_CheckoutScreenState createState() => _CheckoutScreenState();
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
class _CheckoutScreenState extends State<CheckoutScreen> {
|
|
1515
|
+
// User has filled in address, payment info, selected shipping...
|
|
1516
|
+
// But pressing back just pops the screen with no warning
|
|
1517
|
+
|
|
1518
|
+
@override
|
|
1519
|
+
Widget build(BuildContext context) {
|
|
1520
|
+
return Scaffold(
|
|
1521
|
+
appBar: AppBar(title: Text('Checkout')),
|
|
1522
|
+
body: CheckoutForm(), // No PopScope wrapper!
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
**Why developers do it:**
|
|
1529
|
+
|
|
1530
|
+
Navigation "just works" by default -- the back button pops the current route. Protecting against accidental back is seen as optional polish rather than critical UX. The migration from `WillPopScope` to `PopScope` in Flutter 3.12 confused many developers, and some skipped back-button handling entirely.
|
|
1531
|
+
|
|
1532
|
+
**What goes wrong:**
|
|
1533
|
+
|
|
1534
|
+
- Users accidentally lose form data, unsaved edits, or in-progress transactions.
|
|
1535
|
+
- On Android 13+, the predictive back gesture animation plays even when the pop should be blocked, as documented in Flutter issue #140869 and #139050.
|
|
1536
|
+
- Deep links that should navigate to a specific screen within a stack instead navigate to the screen with no back stack, trapping the user.
|
|
1537
|
+
- Double-tapping back on the home screen exits the app without confirmation.
|
|
1538
|
+
|
|
1539
|
+
**The fix:**
|
|
1540
|
+
|
|
1541
|
+
```dart
|
|
1542
|
+
// GOOD: PopScope with confirmation dialog (Flutter 3.12+)
|
|
1543
|
+
class CheckoutScreen extends StatefulWidget {
|
|
1544
|
+
@override
|
|
1545
|
+
_CheckoutScreenState createState() => _CheckoutScreenState();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
class _CheckoutScreenState extends State<CheckoutScreen> {
|
|
1549
|
+
bool _hasUnsavedChanges = true;
|
|
1550
|
+
|
|
1551
|
+
Future<bool> _confirmDiscard() async {
|
|
1552
|
+
if (!_hasUnsavedChanges) return true;
|
|
1553
|
+
final result = await showDialog<bool>(
|
|
1554
|
+
context: context,
|
|
1555
|
+
builder: (context) => AlertDialog(
|
|
1556
|
+
title: const Text('Discard changes?'),
|
|
1557
|
+
content: const Text('You have unsaved checkout information.'),
|
|
1558
|
+
actions: [
|
|
1559
|
+
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
|
1560
|
+
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Discard')),
|
|
1561
|
+
],
|
|
1562
|
+
),
|
|
1563
|
+
);
|
|
1564
|
+
return result ?? false;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
@override
|
|
1568
|
+
Widget build(BuildContext context) {
|
|
1569
|
+
return PopScope(
|
|
1570
|
+
canPop: !_hasUnsavedChanges,
|
|
1571
|
+
onPopInvokedWithResult: (didPop, result) async {
|
|
1572
|
+
if (didPop) return;
|
|
1573
|
+
final shouldPop = await _confirmDiscard();
|
|
1574
|
+
if (shouldPop && mounted) Navigator.of(context).pop();
|
|
1575
|
+
},
|
|
1576
|
+
child: Scaffold(
|
|
1577
|
+
appBar: AppBar(title: const Text('Checkout')),
|
|
1578
|
+
body: CheckoutForm(),
|
|
1579
|
+
),
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
**Detection rule:**
|
|
1586
|
+
If a screen handles user input (forms, editors, multi-step flows) but contains no `PopScope` or `WillPopScope` wrapper, suspect AP-19. If `WillPopScope` is used on Flutter 3.12+, it is deprecated and should be migrated to `PopScope`.
|
|
1587
|
+
|
|
1588
|
+
---
|
|
1589
|
+
|
|
1590
|
+
### AP-20: Using Strings for Everything (No Type Safety)
|
|
1591
|
+
|
|
1592
|
+
**Also known as:** Stringly-Typed Code, The Dynamic Map Plague, Type Erasure
|
|
1593
|
+
**Frequency:** Common
|
|
1594
|
+
**Severity:** Medium
|
|
1595
|
+
**Detection difficulty:** Moderate
|
|
1596
|
+
|
|
1597
|
+
**What it looks like:**
|
|
1598
|
+
|
|
1599
|
+
Passing data as `Map<String, dynamic>` throughout the app instead of defining typed model classes. Using string literals for routes, event names, and configuration keys.
|
|
1600
|
+
|
|
1601
|
+
```dart
|
|
1602
|
+
// BAD: Stringly-typed data flow
|
|
1603
|
+
Future<Map<String, dynamic>> fetchUser() async {
|
|
1604
|
+
final response = await http.get(Uri.parse('/api/user'));
|
|
1605
|
+
return jsonDecode(response.body) as Map<String, dynamic>;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Later in a widget:
|
|
1609
|
+
Text(userData['name'] as String), // Typo-prone
|
|
1610
|
+
Text(userData['adress'] as String), // Misspelled key -- runtime crash
|
|
1611
|
+
Text('${userData['age']}'), // No type checking
|
|
1612
|
+
|
|
1613
|
+
// BAD: String-based navigation
|
|
1614
|
+
Navigator.pushNamed(context, '/usr/profile'); // Typo -- silent failure
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
**Why developers do it:**
|
|
1618
|
+
|
|
1619
|
+
`jsonDecode` returns `Map<String, dynamic>` by default, and it works immediately. Creating model classes for every API response feels like boilerplate. Dynamic typing is faster for prototyping. Coming from JavaScript/Python, developers are accustomed to working with maps and dictionaries.
|
|
1620
|
+
|
|
1621
|
+
**What goes wrong:**
|
|
1622
|
+
|
|
1623
|
+
- Typos in string keys cause runtime crashes, not compile-time errors: `userData['adress']` returns null silently.
|
|
1624
|
+
- Refactoring is dangerous: renaming a field requires finding all string references manually; the compiler cannot help.
|
|
1625
|
+
- No IDE autocomplete for map keys.
|
|
1626
|
+
- No documentation of data shape -- new developers must read API docs or print the map to understand its structure.
|
|
1627
|
+
- The `as String` casts sprinkled everywhere are each a potential `TypeError` at runtime.
|
|
1628
|
+
|
|
1629
|
+
**The fix:**
|
|
1630
|
+
|
|
1631
|
+
```dart
|
|
1632
|
+
// GOOD: Typed model classes
|
|
1633
|
+
class User {
|
|
1634
|
+
final String name;
|
|
1635
|
+
final String email;
|
|
1636
|
+
final int age;
|
|
1637
|
+
final Address address;
|
|
1638
|
+
|
|
1639
|
+
const User({
|
|
1640
|
+
required this.name,
|
|
1641
|
+
required this.email,
|
|
1642
|
+
required this.age,
|
|
1643
|
+
required this.address,
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
factory User.fromJson(Map<String, dynamic> json) {
|
|
1647
|
+
return User(
|
|
1648
|
+
name: json['name'] as String,
|
|
1649
|
+
email: json['email'] as String,
|
|
1650
|
+
age: json['age'] as int,
|
|
1651
|
+
address: Address.fromJson(json['address'] as Map<String, dynamic>),
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Usage: compile-time safety
|
|
1657
|
+
final user = User.fromJson(jsonData);
|
|
1658
|
+
Text(user.name); // Autocomplete, type-safe
|
|
1659
|
+
Text(user.email); // Cannot misspell
|
|
1660
|
+
Text('${user.age}'); // Guaranteed int
|
|
1661
|
+
|
|
1662
|
+
// GOOD: Type-safe routing
|
|
1663
|
+
enum AppRoute { home, profile, settings }
|
|
1664
|
+
// Or use go_router with typed routes
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
**Detection rule:**
|
|
1668
|
+
If `Map<String, dynamic>` appears as a parameter type or return type outside of JSON serialization boundaries, suspect AP-20. If string literals are used for route names and appear in more than one file, it is AP-20. If `as String`, `as int`, `as double` casts appear frequently outside `fromJson` factories, it is AP-20.
|
|
1669
|
+
|
|
1670
|
+
---
|
|
1671
|
+
|
|
1672
|
+
### AP-21: Not Testing Widgets
|
|
1673
|
+
|
|
1674
|
+
**Also known as:** The "It Works on My Phone" Syndrome, Test-Free UI, Manual-Only QA
|
|
1675
|
+
**Frequency:** Very Common
|
|
1676
|
+
**Severity:** High
|
|
1677
|
+
**Detection difficulty:** Easy
|
|
1678
|
+
|
|
1679
|
+
**What it looks like:**
|
|
1680
|
+
|
|
1681
|
+
An app with zero or near-zero widget tests, relying entirely on manual testing or only unit tests for business logic.
|
|
1682
|
+
|
|
1683
|
+
```
|
|
1684
|
+
# Project structure -- BAD: no widget tests
|
|
1685
|
+
lib/
|
|
1686
|
+
screens/
|
|
1687
|
+
home_screen.dart
|
|
1688
|
+
profile_screen.dart
|
|
1689
|
+
settings_screen.dart
|
|
1690
|
+
widgets/
|
|
1691
|
+
user_card.dart
|
|
1692
|
+
product_tile.dart
|
|
1693
|
+
test/
|
|
1694
|
+
services/
|
|
1695
|
+
auth_service_test.dart # Only service tests exist
|
|
1696
|
+
api_client_test.dart
|
|
1697
|
+
# No widget tests at all!
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
**Why developers do it:**
|
|
1701
|
+
|
|
1702
|
+
Widget tests require wrapping widgets in `MaterialApp`, providing mock dependencies, and calling `pumpAndSettle()` -- setup that feels heavyweight compared to unit tests. Developers assume "if the logic is tested and it looks right on the emulator, it works." Widget test documentation is extensive but intimidating for beginners.
|
|
1703
|
+
|
|
1704
|
+
**What goes wrong:**
|
|
1705
|
+
|
|
1706
|
+
- Overflow errors on different screen sizes are only caught when users report them.
|
|
1707
|
+
- State management bugs (wrong widget rebuilding, stale state) are invisible without automated tests.
|
|
1708
|
+
- Regression bugs appear silently: a refactored widget renders differently but no test catches it.
|
|
1709
|
+
- Accessibility issues (missing semantics labels, broken screen reader order) are never detected.
|
|
1710
|
+
- As documented on QuickCoder.org, common pitfalls include overflow errors only appearing in tests (different default test viewport size), animations not triggering without `pump()`, and incorrect finder usage with Material button named constructors.
|
|
1711
|
+
|
|
1712
|
+
**The fix:**
|
|
1713
|
+
|
|
1714
|
+
Write widget tests for every screen and every reusable widget.
|
|
1715
|
+
|
|
1716
|
+
```dart
|
|
1717
|
+
// GOOD: Widget test
|
|
1718
|
+
void main() {
|
|
1719
|
+
testWidgets('UserCard displays name and email', (tester) async {
|
|
1720
|
+
const user = User(name: 'Alice', email: 'alice@example.com');
|
|
1721
|
+
|
|
1722
|
+
await tester.pumpWidget(
|
|
1723
|
+
const MaterialApp(
|
|
1724
|
+
home: Scaffold(body: UserCard(user: user)),
|
|
1725
|
+
),
|
|
1726
|
+
);
|
|
1727
|
+
|
|
1728
|
+
expect(find.text('Alice'), findsOneWidget);
|
|
1729
|
+
expect(find.text('alice@example.com'), findsOneWidget);
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
testWidgets('UserCard tap navigates to profile', (tester) async {
|
|
1733
|
+
const user = User(name: 'Alice', email: 'alice@example.com');
|
|
1734
|
+
|
|
1735
|
+
await tester.pumpWidget(
|
|
1736
|
+
MaterialApp(
|
|
1737
|
+
home: Scaffold(body: UserCard(user: user)),
|
|
1738
|
+
routes: {'/profile': (_) => const ProfileScreen()},
|
|
1739
|
+
),
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
await tester.tap(find.byType(UserCard));
|
|
1743
|
+
await tester.pumpAndSettle();
|
|
1744
|
+
|
|
1745
|
+
expect(find.byType(ProfileScreen), findsOneWidget);
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
**Detection rule:**
|
|
1751
|
+
If the `test/` directory contains no `*_test.dart` files that import `flutter_test`, suspect AP-21. If the ratio of widget test files to widget files (`lib/widgets/` + `lib/screens/`) is less than 0.3, it is AP-21. If `testWidgets` appears nowhere in the test directory, it is AP-21.
|
|
1752
|
+
|
|
1753
|
+
---
|
|
1754
|
+
|
|
1755
|
+
## Root Cause Analysis
|
|
1756
|
+
|
|
1757
|
+
| Anti-Pattern | Root Cause | Prevention |
|
|
1758
|
+
|-------------|------------|------------|
|
|
1759
|
+
| AP-01: God Widget | Laziness / Ignorance | Enforce max widget file length in linter; code review checklist |
|
|
1760
|
+
| AP-02: Methods not Widgets | Cargo culting from other frameworks | DCM `avoid-returning-widgets` lint rule |
|
|
1761
|
+
| AP-03: Missing const | Ignorance of Flutter internals | Enable `prefer_const_constructors` lint |
|
|
1762
|
+
| AP-04: setState for everything | Laziness / Tutorial-driven development | Introduce state management early; architecture templates |
|
|
1763
|
+
| AP-05: Missing dispose | Ignorance / Forgetfulness | `cancel_subscriptions` and `close_sinks` lints; code review |
|
|
1764
|
+
| AP-06: Blocking UI thread | Ignorance (async != parallel) | Profile with DevTools; educate on isolates |
|
|
1765
|
+
| AP-07: GlobalKey overuse | Cargo culting from Form tutorials | Code review; prefer callbacks and state management |
|
|
1766
|
+
| AP-08: Missing list keys | Ignorance of Flutter reconciliation | Lint for missing keys in list builders |
|
|
1767
|
+
| AP-09: Context across async | Ignorance / Race condition blindness | `use_build_context_synchronously` lint |
|
|
1768
|
+
| AP-10: Partial snapshot handling | Laziness / Happy-path development | Template for FutureBuilder/StreamBuilder usage |
|
|
1769
|
+
| AP-11: Hardcoded dimensions | Laziness / Single-device testing | Test on multiple screen sizes; design review |
|
|
1770
|
+
| AP-12: Unnecessary rebuilds | Ignorance of rebuild mechanics | Flutter DevTools Rebuild Stats; const widgets |
|
|
1771
|
+
| AP-13: Logic in widgets | Laziness / Premature shortcuts | Architecture templates; code review for imports |
|
|
1772
|
+
| AP-14: Futures in build | Copy-paste from AI/Stack Overflow | FutureBuilder lint; code review |
|
|
1773
|
+
| AP-15: Overusing StatefulWidget | Ignorance / "Just in case" thinking | Start StatelessWidget, convert when needed |
|
|
1774
|
+
| AP-16: Bang operator abuse | Laziness / Migration shortcuts | Limit `!` per file; code review |
|
|
1775
|
+
| AP-17: Ignoring platform diffs | Ignorance / Single-platform testing | Test on both platforms; platform checklist |
|
|
1776
|
+
| AP-18: Package bloat | Laziness / "There's a package for that" | Review pubspec.yaml in PRs; 20-line rule |
|
|
1777
|
+
| AP-19: Bad navigation handling | Ignorance / "It just works" assumption | Navigation checklist for forms and flows |
|
|
1778
|
+
| AP-20: Stringly-typed code | Laziness / Prototype not refactored | Enforce model classes; ban `Map<String, dynamic>` outside serialization |
|
|
1779
|
+
| AP-21: No widget tests | Laziness / "Looks right on emulator" | CI/CD coverage gates; widget test templates |
|
|
1780
|
+
|
|
1781
|
+
## Self-Check Questions
|
|
1782
|
+
|
|
1783
|
+
1. Does this widget file exceed 200 lines? Should I split it into composed sub-widgets?
|
|
1784
|
+
2. Am I returning `Widget` from a private method? Should this be a separate `StatelessWidget` class instead?
|
|
1785
|
+
3. Can I add `const` to this constructor or widget instantiation? If not, why not?
|
|
1786
|
+
4. Am I using `setState` to manage data that other widgets also need? Should I use a state management solution?
|
|
1787
|
+
5. For every controller, subscription, timer, or focus node I create in `initState`, do I have a corresponding cleanup in `dispose()`?
|
|
1788
|
+
6. Will this computation take more than 16ms? Should it run in an `Isolate` instead of the main thread?
|
|
1789
|
+
7. Am I using `BuildContext` after an `await`? Have I checked `mounted` first?
|
|
1790
|
+
8. Does my `FutureBuilder`/`StreamBuilder` handle loading, error, and empty-data states, not just the success case?
|
|
1791
|
+
9. Am I creating a `Future` or `Stream` inside `build()`? Should it be in `initState` instead?
|
|
1792
|
+
10. Am I using `!` to silence a null warning? Can I handle the null case properly with `??`, `?.`, or a null check?
|
|
1793
|
+
11. Have I tested this screen on both iOS and Android? On a small screen and a large screen?
|
|
1794
|
+
12. Could I implement this package's functionality in under 20 lines of Dart?
|
|
1795
|
+
13. Does this screen with user input have back-button protection (`PopScope`)?
|
|
1796
|
+
14. Am I passing `Map<String, dynamic>` where a typed model class would provide compile-time safety?
|
|
1797
|
+
15. Do I have at least one widget test for every screen in this app?
|
|
1798
|
+
|
|
1799
|
+
## Code Smell Quick Reference
|
|
1800
|
+
|
|
1801
|
+
| If you see... | Suspect... | Verify... |
|
|
1802
|
+
|---------------|-----------|-----------|
|
|
1803
|
+
| `build()` method > 80 lines | AP-01: God Widget | Can sub-trees be extracted to separate widget classes? |
|
|
1804
|
+
| `Widget _build*()` private methods | AP-02: Method not Widget | Replace with `StatelessWidget` class for rebuild optimization |
|
|
1805
|
+
| Widget constructor without `const` keyword when all args are constant | AP-03: Missing const | Add `const`; enable `prefer_const_constructors` lint |
|
|
1806
|
+
| `setState` called 5+ times in one State class | AP-04: setState overload | Introduce Provider/Riverpod/BLoC for shared state |
|
|
1807
|
+
| `TextEditingController()` / `StreamSubscription` without matching `dispose()` | AP-05: Missing dispose | Add `dispose()` with cleanup for every resource |
|
|
1808
|
+
| Loops, JSON parsing, or file I/O inside `build()` | AP-06: UI thread blocked | Move to `Isolate.run()` or `compute()` |
|
|
1809
|
+
| `GlobalKey()` inside `build()` | AP-07: GlobalKey in build | Move to field or `initState`; prefer callbacks |
|
|
1810
|
+
| `ListView.builder` items without `key:` parameter | AP-08: Missing keys | Add `ValueKey(item.id)` with stable identifier |
|
|
1811
|
+
| `context` used after `await` without `mounted` check | AP-09: Stale context | Add `if (!mounted) return;` after every `await` |
|
|
1812
|
+
| `FutureBuilder` without `ConnectionState.waiting` check | AP-10: Partial state handling | Handle waiting, error, no-data, and success states |
|
|
1813
|
+
| Literal numbers > 100 for width/height | AP-11: Hardcoded dimensions | Use `LayoutBuilder`, `MediaQuery`, or `Flexible` |
|
|
1814
|
+
| `setState` modifies 1 field but `build()` creates 10+ widgets | AP-12: Rebuild avalanche | Push state down to smallest widget; use `const` children |
|
|
1815
|
+
| Widget importing `http`, `dart:convert`, database packages | AP-13: Logic in widget | Extract to service/repository class |
|
|
1816
|
+
| `FutureBuilder(future: fetchData(),` inside `build()` | AP-14: Future in build | Create Future in `initState`, reference it in builder |
|
|
1817
|
+
| `StatefulWidget` with no mutable fields in State | AP-15: Unnecessary StatefulWidget | Convert to `StatelessWidget` |
|
|
1818
|
+
| Multiple `!` operators on same line | AP-16: Bang abuse | Use `?.`, `??`, null checks, or early return |
|
|
1819
|
+
| No `Platform.isIOS` / `SafeArea` in cross-platform app | AP-17: Platform blindness | Test on both platforms; add platform-specific handling |
|
|
1820
|
+
| 15+ dependencies in `pubspec.yaml` | AP-18: Package bloat | Review each: can it be implemented in < 20 lines? |
|
|
1821
|
+
| Form screen without `PopScope` wrapper | AP-19: Unprotected navigation | Add `PopScope` with confirmation dialog |
|
|
1822
|
+
| `Map<String, dynamic>` passed between methods | AP-20: Stringly-typed | Create typed model class with `fromJson` factory |
|
|
1823
|
+
| No `testWidgets` in test directory | AP-21: No widget tests | Add widget tests for screens and reusable widgets |
|
|
1824
|
+
|
|
1825
|
+
---
|
|
1826
|
+
|
|
1827
|
+
*Researched: 2026-03-08 | Sources: [Flutter Performance Best Practices](https://docs.flutter.dev/perf/best-practices), [DCM Anti-Patterns](https://dartcodemetrics.dev/docs/anti-patterns), [DCM: Memory Leaks in Dart and Flutter](https://dcm.dev/blog/2024/10/21/lets-talk-about-memory-leaks-in-dart-and-flutter/), [DCM: 15 Common Mistakes](https://dcm.dev/blog/2025/03/24/fifteen-common-mistakes-flutter-dart-development/), [Splitting Widgets to Methods is an Anti-Pattern (Iiro Krankka)](https://iiro.dev/splitting-widgets-to-methods-performance-antipattern/), [Flutter Widget Rebuild Optimization](https://763p.me/blog/2025/09/28/mastering-flutter-rebuild-optimization-eliminating-unnecessary-widget-rebuilds/), [Flutter Concurrency and Isolates](https://docs.flutter.dev/perf/isolates), [Flutter GlobalKey Documentation](https://api.flutter.dev/flutter/widgets/GlobalKey-class.html), [Flutter GlobalKey Performance Issue #35730](https://github.com/flutter/flutter/issues/35730), [FutureBuilder Common Mistakes](https://medium.com/@wartelski/futurebuilder-in-flutter-mistakes-you-might-be-making-e97209f66c2f), [BuildContext Across Async Gaps](https://dart.dev/tools/linter-rules/use_build_context_synchronously), [Flutter PopScope Breaking Changes](https://docs.flutter.dev/release/breaking-changes/android-predictive-back), [PopScope Issue #140869](https://github.com/flutter/flutter/issues/140869), [Flutter Architecture Guide](https://docs.flutter.dev/app-architecture/guide), [Effective Dart: Design](https://dart.dev/effective-dart/design), [Widget Testing Introduction](https://docs.flutter.dev/cookbook/testing/widget/introduction), [Widget Testing Pitfalls (QuickCoder)](https://quickcoder.org/a-short-excursion-into-the-pitfalls-of-flutter-widget-testing/), [Stop Doing These Flutter Performance Mistakes (2026)](https://medium.com/@tiger.chirag/stop-doing-these-flutter-performance-mistakes-2026-edition-79cae09d5f22), [Flutter State Management Guide (2026)](https://medium.com/@satishparmarparmar486/the-ultimate-guide-to-flutter-state-management-in-2026-from-setstate-to-bloc-riverpod-561192c31e1c), [Flutter Responsive Design](https://docs.flutter.dev/ui/adaptive-responsive/general)*
|