Refactor FormField build wrappers (#185795)

## Summary

This is the refactor-only half of the sliver form field work.

`FormFieldState.build` now delegates its validation semantics wrapper
and `AutovalidateMode.onUnfocus` focus wrapper through protected
`wrapWithSemantics` and `wrapWithFocus` methods. The shared validation
result and `Focus.includeSemantics` value are kept behind small getters
so subclasses can customize only the layout-protocol-specific pieces.

There is no intended behavior change for `FormField` or existing
subclasses. The existing render-box semantics and focus behavior remain
the same.

## Issue

Preparation for #173912. This PR does not fix the issue by itself; the
sliver API can follow in a separate PR after this refactor lands.

## Tests

- `flutter analyze --no-pub packages/flutter/lib/src/widgets/form.dart
packages/flutter/test/widgets/form_test.dart
packages/flutter/test/widgets/semantics_test.dart
packages/flutter/test/material/text_form_field_test.dart
packages/flutter/test/material/text_form_field_restoration_test.dart
packages/flutter/test/material/dropdown_form_field_test.dart
packages/flutter/test/material/dropdown_menu_form_field_test.dart
packages/flutter/test/material/input_date_picker_form_field_test.dart`
- `flutter test packages/flutter/test/widgets/form_test.dart
packages/flutter/test/widgets/semantics_test.dart
packages/flutter/test/material/text_form_field_test.dart
packages/flutter/test/material/text_form_field_restoration_test.dart
packages/flutter/test/material/dropdown_form_field_test.dart
packages/flutter/test/material/dropdown_menu_form_field_test.dart
packages/flutter/test/material/input_date_picker_form_field_test.dart`

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide].
- [x] I listed at least one issue that this PR relates to in the
description above.
- [x] I added or updated tests where needed for the change I am making.
- [x] All existing and new tests are passing.

[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo

---------

Co-authored-by: Victor Sanni <victorsanniay@gmail.com>
diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart
index 7ec722e..190fa79 100644
--- a/packages/flutter/lib/src/widgets/form.dart
+++ b/packages/flutter/lib/src/widgets/form.dart
@@ -635,6 +635,10 @@
 
 /// The current state of a [FormField]. Passed to the [FormFieldBuilder] method
 /// for use in constructing the form field's widget.
+///
+/// Subclasses whose [FormField.builder] returns a sliver can override
+/// [wrapWithSemantics], [wrapWithFocus], and [focusIncludesSemantics] to
+/// replace the default render-box wrappers with sliver-compatible ones.
 class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
   late T? _value = widget.initialValue;
   // Marking it as late, so it can be registered
@@ -829,6 +833,78 @@
     super.dispose();
   }
 
+  /// The [SemanticsValidationResult] published by [wrapWithSemantics] for this
+  /// field's current state.
+  ///
+  /// See also:
+  ///
+  ///  * [wrapWithSemantics], which publishes this value on a [Semantics] node.
+  ///  * [hasError], which the default implementation reflects.
+  @protected
+  SemanticsValidationResult get semanticsValidationResult =>
+      hasError ? SemanticsValidationResult.invalid : SemanticsValidationResult.valid;
+
+  /// Whether [wrapWithFocus]'s [Focus] should add its own [Semantics] node.
+  ///
+  /// Subclasses whose [wrapWithSemantics] uses a sliver-typed semantics node
+  /// override this to `false`, since the box-typed semantics node added by
+  /// [Focus] would otherwise crash on a sliver child.
+  ///
+  /// See also:
+  ///
+  ///  * [wrapWithFocus], whose [Focus] node this flag configures.
+  ///  * [Focus.includeSemantics], which receives this value.
+  @protected
+  bool get focusIncludesSemantics => true;
+
+  /// Wraps the result of [FormField.builder] with a [Semantics] node that
+  /// publishes the field's [SemanticsValidationResult].
+  ///
+  /// Subclasses whose builder returns a sliver override this to wrap with a
+  /// sliver-typed semantics node instead, since [Semantics] is a render-box
+  /// widget and would crash when given a sliver child.
+  ///
+  /// See also:
+  ///
+  ///  * [semanticsValidationResult], the value the default implementation
+  ///    publishes.
+  ///  * [wrapWithFocus], which wraps the result of this method when
+  ///    [AutovalidateMode.onUnfocus] validation is in effect.
+  @protected
+  Widget wrapWithSemantics(Widget child) {
+    return Semantics(validationResult: semanticsValidationResult, child: child);
+  }
+
+  /// Wraps [child] with the [Focus] node used to drive
+  /// [AutovalidateMode.onUnfocus] validation.
+  ///
+  /// Subclasses whose builder returns a sliver override [focusIncludesSemantics]
+  /// to disable [Focus]'s own [Semantics] wrapper, which is a render-box widget.
+  ///
+  /// See also:
+  ///
+  ///  * [focusIncludesSemantics], which controls whether the [Focus] node adds
+  ///    its own [Semantics] node.
+  ///  * [wrapWithSemantics], which wraps the result of [FormField.builder]
+  ///    before this method runs.
+  @protected
+  Widget wrapWithFocus(Widget child) {
+    return Focus(
+      canRequestFocus: false,
+      skipTraversal: true,
+      includeSemantics: focusIncludesSemantics,
+      onFocusChange: (bool value) {
+        if (!value) {
+          setState(() {
+            _validate();
+          });
+        }
+      },
+      focusNode: _focusNode,
+      child: child,
+    );
+  }
+
   @protected
   @override
   Widget build(BuildContext context) {
@@ -852,29 +928,12 @@
 
     Form.maybeOf(context)?._register(this);
 
-    final Widget child = Semantics(
-      validationResult: hasError
-          ? SemanticsValidationResult.invalid
-          : SemanticsValidationResult.valid,
-      child: widget.builder(this),
-    );
+    final Widget child = wrapWithSemantics(widget.builder(this));
 
     if (Form.maybeOf(context)?.widget.autovalidateMode == AutovalidateMode.onUnfocus &&
             widget.autovalidateMode != AutovalidateMode.always ||
         widget.autovalidateMode == AutovalidateMode.onUnfocus) {
-      return Focus(
-        canRequestFocus: false,
-        skipTraversal: true,
-        onFocusChange: (bool value) {
-          if (!value) {
-            setState(() {
-              _validate();
-            });
-          }
-        },
-        focusNode: _focusNode,
-        child: child,
-      );
+      return wrapWithFocus(child);
     }
 
     return child;
diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart
index 257ee11..fc59d4e 100644
--- a/packages/flutter/test/widgets/form_test.dart
+++ b/packages/flutter/test/widgets/form_test.dart
@@ -1901,6 +1901,52 @@
     );
   });
 
+  testWidgets('FormFieldState.build delegates semantics wrapping to wrapWithSemantics', (
+    WidgetTester tester,
+  ) async {
+    final SemanticsHandle handle = tester.ensureSemantics();
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: Form(
+          child: _WrappingFormField(semanticsValidationResult: SemanticsValidationResult.invalid),
+        ),
+      ),
+    );
+
+    expect(find.byKey(_semanticsWrapperKey), findsOneWidget);
+    expect(find.byKey(_focusWrapperKey), findsNothing);
+    expect(
+      tester.getSemantics(find.text('field')),
+      containsSemantics(validationResult: SemanticsValidationResult.invalid),
+    );
+
+    handle.dispose();
+  });
+
+  testWidgets('FormFieldState.build delegates onUnfocus wrapping to wrapWithFocus', (
+    WidgetTester tester,
+  ) async {
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: Form(
+          autovalidateMode: AutovalidateMode.onUnfocus,
+          child: _WrappingFormField(focusIncludesSemantics: false),
+        ),
+      ),
+    );
+
+    expect(find.byKey(_focusWrapperKey), findsOneWidget);
+    expect(find.byKey(_semanticsWrapperKey), findsOneWidget);
+    final Finder focus = find.descendant(
+      of: find.byKey(_focusWrapperKey),
+      matching: find.byType(Focus),
+    );
+    expect(tester.widget<Focus>(focus).includeSemantics, isFalse);
+  });
+
   testWidgets('Form does not crash at zero area', (WidgetTester tester) async {
     await tester.pumpWidget(
       const Directionality(
@@ -2149,3 +2195,39 @@
   final bool supportsAnnounce;
   final String testName;
 }
+
+const Key _semanticsWrapperKey = Key('semantics wrapper');
+const Key _focusWrapperKey = Key('focus wrapper');
+
+class _WrappingFormField extends FormField<String> {
+  _WrappingFormField({
+    this.semanticsValidationResult = SemanticsValidationResult.valid,
+    this.focusIncludesSemantics = true,
+  }) : super(initialValue: 'value', builder: (FormFieldState<String> state) => const Text('field'));
+
+  final SemanticsValidationResult semanticsValidationResult;
+  final bool focusIncludesSemantics;
+
+  @override
+  FormFieldState<String> createState() => _WrappingFormFieldState();
+}
+
+class _WrappingFormFieldState extends FormFieldState<String> {
+  _WrappingFormField get _widget => widget as _WrappingFormField;
+
+  @override
+  SemanticsValidationResult get semanticsValidationResult => _widget.semanticsValidationResult;
+
+  @override
+  bool get focusIncludesSemantics => _widget.focusIncludesSemantics;
+
+  @override
+  Widget wrapWithSemantics(Widget child) {
+    return KeyedSubtree(key: _semanticsWrapperKey, child: super.wrapWithSemantics(child));
+  }
+
+  @override
+  Widget wrapWithFocus(Widget child) {
+    return KeyedSubtree(key: _focusWrapperKey, child: super.wrapWithFocus(child));
+  }
+}