Focused Agent Skills for complete Flutter and Dart app delivery.
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Screen readers (TalkBack on Android, VoiceOver on iOS/macOS) translate Flutter's semantics tree into spoken feedback and braille output. Proper semantics configuration ensures all users can navigate and operate the app.
Flutter automatically builds a semantics tree from built-in widgets (e.g., Text, ElevatedButton, Checkbox). Only add manual Semantics widgets when custom interactive or composite components are used.
SemanticsUse to annotate custom interactive elements with label, value, role, or custom actions. Share the same enabled condition across touch, keyboard, and semantics. enabled describes the state while the presence of onTap exposes an action. When one wrapper owns the semantics for the entire control, exclude duplicate child semantics. Preserve meaningful child nodes in a composite control.
final activate = isActionable ? onAddToCart : null;
Semantics(
button: true,
enabled: isActionable,
label: 'Add item to shopping cart',
onTap: activate,
excludeSemantics: true,
child: CustomCartButton(onPressed: activate),
)The custom control uses a null onPressed to disable pointer and keyboard activation. Test the disabled node's label and state, absence of a tap action, and unchanged application state after attempted activation. Verify that the enabled control exposes and dispatches one action. See Semantics.enabled and Semantics.onTap.
MergeSemanticsMerge a subtree of related nodes into a single semantic focus node. Essential for list tiles or cards containing an icon, title, and subtitle so the screen reader speaks the entire group together instead of forcing three separate swipes:
MergeSemantics(
child: Row(
children: [
Icon(Icons.check_circle),
SizedBox(width: 8),
Text('Order Placed'),
],
),
)ExcludeSemantics / BlockSemanticsExcludeSemantics: Hide purely decorative elements (icons with adjacent text, background illustrations, spacer art) from the screen reader to reduce audio clutter.BlockSemantics: Drop semantics behind a modal surface (dialog, bottom sheet, drawer) to prevent screen readers from focusing on backdrop widgets.Prefer semantic state changes, including a suitable live region, for ordinary asynchronous feedback. Use an explicit announcement only when the system does not already announce the change.
A save confirmation, form result, or loading-finished message is ordinary feedback. Show it as visible status text inside Semantics(liveRegion: true). On Android and iOS a live region produces a polite announcement without moving focus, though the OS may skip it while already speaking. Verify web behavior on the pinned SDK with a screen reader. Do not pick an explicit announcement for it per platform. Keep sendAnnouncement for changes that have no visible semantic node. A request to fix a failing announce call does not mean an announcement is the right mechanism: fix the call, then say whether the feedback should move to a semantic update. On SDKs supporting the current view-aware API:
if (MediaQuery.supportsAnnounceOf(context)) {
SemanticsService.sendAnnouncement(
View.of(context),
'Changes saved successfully', // Use the project's localized message.
Directionality.of(context),
assertiveness: Assertiveness.polite,
);
}Directionality.of(context) instead of hardcoding LTR to preserve RTL locale support.sendAnnouncement or supportsAnnounceOf. The deprecated announce API relies on an implicit view and is incompatible with multiple windows; retain a legacy path only when the supported SDK requires it and its single-view contract is satisfied.assertiveness currently affects web only. On Android, prefer semantic updates because explicit announcement events can disrupt TalkBack's speech queue. Avoid duplicate or repetitive announcements.See SemanticsService.sendAnnouncement and the legacy API limitations.
testWidgets enables semantics by default and disposes its framework-owned handle after the callback. Prefer that default instead of opening a second handle:
testWidgets('favorite toggle exposes its state', (tester) async {
await tester.pumpWidget(const TestApp());
expect(
tester.getSemantics(find.byType(CustomCartButton)),
matchesSemantics(...),
);
});final handle = tester.ensureSemantics();
addTearDown(handle.dispose);meetsGuideline(androidTapTargetGuideline) and meetsGuideline(iOSTapTargetGuideline) in widget tests..tessl-plugin
skills
dart-concurrency
dart-language
flutter-accessibility
flutter-ai-integration
flutter-animation
flutter-app-workflow
flutter-architecture
flutter-authentication
flutter-background-execution
flutter-build-release
flutter-ci-cd
flutter-code-review
flutter-dependency-upgrades
flutter-device-testing
flutter-figma-workflow
flutter-in-app-purchases
flutter-localization
references
flutter-location-maps
flutter-media
flutter-networking
flutter-notifications
flutter-observability
flutter-openapi-client
flutter-package-development
flutter-payments-checkout
flutter-performance
flutter-persistence
flutter-platform-integration
flutter-product-analytics
flutter-responsive-layout
flutter-runtime-debugging
flutter-security
flutter-state-management
flutter-testing
flutter-text-rendering
references
flutter-ui-design
flutter-ui-patterns
flutter-visual-effects