[autofill] opt-out instead of opt-in (#86312)

diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart
index cbfe083..400d1a4 100644
--- a/packages/flutter/lib/src/cupertino/text_field.dart
+++ b/packages/flutter/lib/src/cupertino/text_field.dart
@@ -293,7 +293,7 @@
     this.onTap,
     this.scrollController,
     this.scrollPhysics,
-    this.autofillHints,
+    this.autofillHints = const <String>[],
     this.restorationId,
     this.enableIMEPersonalizedLearning = true,
   }) : assert(textAlign != null),
@@ -449,7 +449,7 @@
     this.onTap,
     this.scrollController,
     this.scrollPhysics,
-    this.autofillHints,
+    this.autofillHints = const <String>[],
     this.restorationId,
     this.enableIMEPersonalizedLearning = true,
   }) : assert(textAlign != null),
@@ -837,7 +837,7 @@
   }
 }
 
-class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate {
+class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
   final GlobalKey _clearGlobalKey = GlobalKey();
 
   RestorableTextEditingController? _controller;
@@ -1098,6 +1098,28 @@
       },
     );
   }
+  // AutofillClient implementation start.
+  @override
+  String get autofillId => _editableText.autofillId;
+
+  @override
+  void autofill(TextEditingValue newEditingValue) => _editableText.autofill(newEditingValue);
+
+  @override
+  TextInputConfiguration get textInputConfiguration {
+    final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
+    final AutofillConfiguration autofillConfiguration = autofillHints != null
+      ? AutofillConfiguration(
+          uniqueIdentifier: autofillId,
+          autofillHints: autofillHints,
+          currentEditingValue: _effectiveController.value,
+          hintText: widget.placeholder,
+        )
+      : AutofillConfiguration.disabled;
+
+    return _editableText.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
+  }
+  // AutofillClient implementation end.
 
   @override
   Widget build(BuildContext context) {
@@ -1242,7 +1264,7 @@
             scrollController: widget.scrollController,
             scrollPhysics: widget.scrollPhysics,
             enableInteractiveSelection: widget.enableInteractiveSelection,
-            autofillHints: widget.autofillHints,
+            autofillClient: this,
             restorationId: 'editable',
             enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
           ),
diff --git a/packages/flutter/lib/src/material/selectable_text.dart b/packages/flutter/lib/src/material/selectable_text.dart
index 17f6a6b..21e0667 100644
--- a/packages/flutter/lib/src/material/selectable_text.dart
+++ b/packages/flutter/lib/src/material/selectable_text.dart
@@ -692,6 +692,7 @@
         enableInteractiveSelection: widget.enableInteractiveSelection,
         dragStartBehavior: widget.dragStartBehavior,
         scrollPhysics: widget.scrollPhysics,
+        autofillHints: null,
       ),
     );
 
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index dc712fe..caa6c63 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -345,7 +345,7 @@
     this.buildCounter,
     this.scrollController,
     this.scrollPhysics,
-    this.autofillHints,
+    this.autofillHints = const <String>[],
     this.restorationId,
     this.enableIMEPersonalizedLearning = true,
   }) : assert(textAlign != null),
@@ -827,7 +827,7 @@
   }
 }
 
-class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
+class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
   RestorableTextEditingController? _controller;
   TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
 
@@ -1094,6 +1094,29 @@
     }
   }
 
+  // AutofillClient implementation start.
+  @override
+  String get autofillId => _editableText!.autofillId;
+
+  @override
+  void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue);
+
+  @override
+  TextInputConfiguration get textInputConfiguration {
+    final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
+    final AutofillConfiguration autofillConfiguration = autofillHints != null
+      ? AutofillConfiguration(
+          uniqueIdentifier: autofillId,
+          autofillHints: autofillHints,
+          currentEditingValue: _effectiveController.value,
+          hintText: (widget.decoration ?? const InputDecoration()).hintText,
+        )
+      : AutofillConfiguration.disabled;
+
+    return _editableText!.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
+  }
+  // AutofillClient implementation end.
+
   @override
   Widget build(BuildContext context) {
     assert(debugCheckHasMaterial(context));
@@ -1240,7 +1263,7 @@
           dragStartBehavior: widget.dragStartBehavior,
           scrollController: widget.scrollController,
           scrollPhysics: widget.scrollPhysics,
-          autofillHints: widget.autofillHints,
+          autofillClient: this,
           autocorrectionTextRectColor: autocorrectionTextRectColor,
           restorationId: 'editable',
           enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
diff --git a/packages/flutter/lib/src/services/autofill.dart b/packages/flutter/lib/src/services/autofill.dart
index 4a47015..65de3fc 100644
--- a/packages/flutter/lib/src/services/autofill.dart
+++ b/packages/flutter/lib/src/services/autofill.dart
@@ -631,12 +631,40 @@
   /// Creates autofill related configuration information that can be sent to the
   /// platform.
   const AutofillConfiguration({
+    required String uniqueIdentifier,
+    required List<String> autofillHints,
+    required TextEditingValue currentEditingValue,
+    String? hintText,
+  }) : this._(
+    enabled: true,
+    uniqueIdentifier: uniqueIdentifier,
+    autofillHints: autofillHints,
+    currentEditingValue: currentEditingValue,
+    hintText: hintText,
+  );
+
+  const AutofillConfiguration._({
+    required this.enabled,
     required this.uniqueIdentifier,
-    required this.autofillHints,
+    this.autofillHints = const <String>[],
+    this.hintText,
     required this.currentEditingValue,
   }) : assert(uniqueIdentifier != null),
        assert(autofillHints != null);
 
+  /// An [AutofillConfiguration] that indicates the [AutofillClient] does not
+  /// wish to be autofilled.
+  static const AutofillConfiguration disabled = AutofillConfiguration._(
+    enabled: false,
+    uniqueIdentifier: '',
+    currentEditingValue: TextEditingValue.empty,
+  );
+
+  /// Whether autofill should be enabled for the [AutofillClient].
+  ///
+  /// To retrieve a disabled [AutofillConfiguration], use [disabled].
+  final bool enabled;
+
   /// A string that uniquely identifies the current [AutofillClient].
   ///
   /// The identifier needs to be unique within the [AutofillScope] for the
@@ -648,7 +676,7 @@
   /// A list of strings that helps the autofill service identify the type of the
   /// [AutofillClient].
   ///
-  /// Must not be null or empty.
+  /// Must not be null.
   ///
   /// {@template flutter.services.AutofillConfiguration.autofillHints}
   /// For the best results, hint strings need to be understood by the platform's
@@ -697,14 +725,23 @@
   /// The current [TextEditingValue] of the [AutofillClient].
   final TextEditingValue currentEditingValue;
 
+  /// The optional hint text placed on the view that typically suggests what
+  /// sort of input the field accepts, for example "enter your password here".
+  ///
+  /// If the developer does not specify any [autofillHints], the [hintText] can
+  /// be a useful indication to the platform autofill service.
+  final String? hintText;
+
   /// Returns a representation of this object as a JSON object.
-  Map<String, dynamic> toJson() {
-    assert(autofillHints.isNotEmpty);
-    return <String, dynamic>{
-      'uniqueIdentifier': uniqueIdentifier,
-      'hints': autofillHints,
-      'editingValue': currentEditingValue.toJSON(),
-    };
+  Map<String, dynamic>? toJson() {
+    return enabled
+      ? <String, dynamic>{
+          'uniqueIdentifier': uniqueIdentifier,
+          'hints': autofillHints,
+          'editingValue': currentEditingValue.toJSON(),
+          if (hintText != null) 'hintText': hintText,
+        }
+      : null;
   }
 }
 
@@ -715,7 +752,7 @@
 abstract class AutofillClient {
   /// The unique identifier of this [AutofillClient].
   ///
-  /// Must not be null.
+  /// Must not be null and the identifier must not be changed.
   String get autofillId;
 
   /// The [TextInputConfiguration] that describes this [AutofillClient].
@@ -726,7 +763,7 @@
 
   /// Requests this [AutofillClient] update its [TextEditingValue] to the given
   /// value.
-  void updateEditingValue(TextEditingValue newEditingValue);
+  void autofill(TextEditingValue newEditingValue);
 }
 
 /// An ordered group within which [AutofillClient]s are logically connected.
@@ -806,7 +843,7 @@
   TextInputConnection attach(TextInputClient trigger, TextInputConfiguration configuration) {
     assert(trigger != null);
     assert(
-      !autofillClients.any((AutofillClient client) => client.textInputConfiguration.autofillConfiguration == null),
+      !autofillClients.any((AutofillClient client) => !client.textInputConfiguration.autofillConfiguration.enabled),
       'Every client in AutofillScope.autofillClients must enable autofill',
     );
 
diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart
index c19d856..97cf936 100644
--- a/packages/flutter/lib/src/services/text_input.dart
+++ b/packages/flutter/lib/src/services/text_input.dart
@@ -466,7 +466,7 @@
     this.inputAction = TextInputAction.done,
     this.keyboardAppearance = Brightness.light,
     this.textCapitalization = TextCapitalization.none,
-    this.autofillConfiguration,
+    this.autofillConfiguration = AutofillConfiguration.disabled,
     this.enableIMEPersonalizedLearning = true,
   }) : assert(inputType != null),
        assert(obscureText != null),
@@ -503,7 +503,7 @@
   /// to the platform. This will prevent the corresponding input field from
   /// participating in autofills triggered by other fields. Additionally, on
   /// Android and web, setting [autofillConfiguration] to null disables autofill.
-  final AutofillConfiguration? autofillConfiguration;
+  final AutofillConfiguration autofillConfiguration;
 
   /// {@template flutter.services.TextInputConfiguration.smartDashesType}
   /// Whether to allow the platform to automatically format dashes.
@@ -607,8 +607,41 @@
   /// {@endtemplate}
   final bool enableIMEPersonalizedLearning;
 
+  /// Creates a copy of this [TextInputConfiguration] with the given fields
+  /// replaced with new values.
+  TextInputConfiguration copyWith({
+    TextInputType? inputType,
+    bool? readOnly,
+    bool? obscureText,
+    bool? autocorrect,
+    SmartDashesType? smartDashesType,
+    SmartQuotesType? smartQuotesType,
+    bool? enableSuggestions,
+    String? actionLabel,
+    TextInputAction? inputAction,
+    Brightness? keyboardAppearance,
+    TextCapitalization? textCapitalization,
+    bool? enableIMEPersonalizedLearning,
+    AutofillConfiguration? autofillConfiguration,
+  }) {
+    return TextInputConfiguration(
+      inputType: inputType ?? this.inputType,
+      readOnly: readOnly ?? this.readOnly,
+      obscureText: obscureText ?? this.obscureText,
+      autocorrect: autocorrect ?? this.autocorrect,
+      smartDashesType: smartDashesType ?? this.smartDashesType,
+      smartQuotesType: smartQuotesType ?? this.smartQuotesType,
+      enableSuggestions: enableSuggestions ?? this.enableSuggestions,
+      inputAction: inputAction ?? this.inputAction,
+      textCapitalization: textCapitalization ?? this.textCapitalization,
+      keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
+      enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
+      autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
+    );
+  }
   /// Returns a representation of this object as a JSON object.
   Map<String, dynamic> toJson() {
+    final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
     return <String, dynamic>{
       'inputType': inputType.toJson(),
       'readOnly': readOnly,
@@ -622,7 +655,7 @@
       'textCapitalization': textCapitalization.toString(),
       'keyboardAppearance': keyboardAppearance.toString(),
       'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
-      if (autofillConfiguration != null) 'autofill': autofillConfiguration!.toJson(),
+      if (autofill != null) 'autofill': autofill,
     };
   }
 }
@@ -1470,7 +1503,10 @@
         final TextEditingValue textEditingValue = TextEditingValue.fromJSON(
           editingValue[tag] as Map<String, dynamic>,
         );
-        scope?.getAutofillClient(tag)?.updateEditingValue(textEditingValue);
+        final AutofillClient? client = scope?.getAutofillClient(tag);
+        if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) {
+          client.autofill(textEditingValue);
+        }
       }
 
       return;
diff --git a/packages/flutter/lib/src/widgets/autofill.dart b/packages/flutter/lib/src/widgets/autofill.dart
index 40b8647..d105699 100644
--- a/packages/flutter/lib/src/widgets/autofill.dart
+++ b/packages/flutter/lib/src/widgets/autofill.dart
@@ -134,7 +134,7 @@
   @override
   Iterable<AutofillClient> get autofillClients {
     return _clients.values
-      .where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration != null);
+      .where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled);
   }
 
   /// Adds the [AutofillClient] to this [AutofillGroup].
@@ -155,9 +155,8 @@
   /// Removes an [AutofillClient] with the given `autofillId` from this
   /// [AutofillGroup].
   ///
-  /// Typically, this should be called by autofillable [TextInputClient]s in
-  /// [State.dispose] and [State.didChangeDependencies], when the input field
-  /// needs to be removed from the [AutofillGroup] it is currently registered to.
+  /// Typically, this should be called by a text field when it's being disposed,
+  /// or before it's registered with a different [AutofillGroup].
   ///
   /// See also:
   ///
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index 6a657d3..566b233 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -456,7 +456,8 @@
       paste: true,
       selectAll: true,
     ),
-    this.autofillHints,
+    this.autofillHints = const <String>[],
+    this.autofillClient,
     this.clipBehavior = Clip.hardEdge,
     this.restorationId,
     this.scrollBehavior,
@@ -499,10 +500,6 @@
        assert(dragStartBehavior != null),
        assert(toolbarOptions != null),
        assert(clipBehavior != null),
-       assert(
-         !readOnly || autofillHints == null,
-         "Read-only fields can't have autofill hints.",
-       ),
        assert(enableIMEPersonalizedLearning != null),
        _strutStyle = strutStyle,
        keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
@@ -1163,15 +1160,17 @@
   /// A list of strings that helps the autofill service identify the type of this
   /// text input.
   ///
-  /// When set to null or empty, this text input will not send its autofill
-  /// information to the platform, preventing it from participating in
-  /// autofills triggered by a different [AutofillClient], even if they're in the
-  /// same [AutofillScope]. Additionally, on Android and web, setting this to
-  /// null or empty will disable autofill for this text field.
+  /// When set to null, this text input will not send its autofill information
+  /// to the platform, preventing it from participating in autofills triggered
+  /// by a different [AutofillClient], even if they're in the same
+  /// [AutofillScope]. Additionally, on Android and web, setting this to null
+  /// will disable autofill for this text field.
   ///
   /// The minimum platform SDK version that supports Autofill is API level 26
   /// for Android, and iOS 10.0 for iOS.
   ///
+  /// Defaults to an empty list.
+  ///
   /// ### Setting up iOS autofill:
   ///
   /// To provide the best user experience and ensure your app fully supports
@@ -1229,6 +1228,12 @@
   /// {@macro flutter.services.AutofillConfiguration.autofillHints}
   final Iterable<String>? autofillHints;
 
+  /// The [AutofillClient] that controls this input field's autofill behavior.
+  ///
+  /// When null, this widget's [EditableTextState] will be used as the
+  /// [AutofillClient]. This property may override [autofillHints].
+  final AutofillClient? autofillClient;
+
   /// {@macro flutter.material.Material.clipBehavior}
   ///
   /// Defaults to [Clip.hardEdge].
@@ -1278,12 +1283,11 @@
     required Iterable<String>? autofillHints,
     required int? maxLines,
   }) {
-    if (autofillHints?.isEmpty ?? true) {
+    if (autofillHints == null || autofillHints.isEmpty) {
       return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
     }
 
-    TextInputType? returnValue;
-    final String effectiveHint = autofillHints!.first;
+    final String effectiveHint = autofillHints.first;
 
     // On iOS oftentimes specifying a text content type is not enough to qualify
     // the input field for autofill. The keyboard type also needs to be compatible
@@ -1328,7 +1332,10 @@
             AutofillHints.username : TextInputType.text,
           };
 
-          returnValue = iOSKeyboardType[effectiveHint];
+          final TextInputType? keyboardType = iOSKeyboardType[effectiveHint];
+          if (keyboardType != null) {
+            return keyboardType;
+          }
           break;
         case TargetPlatform.android:
         case TargetPlatform.fuchsia:
@@ -1338,8 +1345,9 @@
       }
     }
 
-    if (returnValue != null || maxLines != 1)
-      return returnValue ?? TextInputType.multiline;
+    if (maxLines != 1) {
+      return TextInputType.multiline;
+    }
 
     const Map<String, TextInputType> inferKeyboardType = <String, TextInputType> {
       AutofillHints.addressCity : TextInputType.streetAddress,
@@ -1474,8 +1482,7 @@
   @override
   AutofillScope? get currentAutofillScope => _currentAutofillScope;
 
-  // Is this field in the current autofill context.
-  bool _isInAutofillContext = false;
+  AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this;
 
   /// Whether to create an input connection with the platform for text editing
   /// or not.
@@ -1548,8 +1555,7 @@
     if (currentAutofillScope != newAutofillGroup) {
       _currentAutofillScope?.unregister(autofillId);
       _currentAutofillScope = newAutofillGroup;
-      newAutofillGroup?.register(this);
-      _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
+      _currentAutofillScope?.register(_effectiveAutofillClient);
     }
 
     if (!_didAutoFocus && widget.autofocus) {
@@ -1574,7 +1580,11 @@
       _selectionOverlay?.update(_value);
     }
     _selectionOverlay?.handlesVisible = widget.showSelectionHandles;
-    _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
+
+    if (widget.autofillClient != oldWidget.autofillClient) {
+      _currentAutofillScope?.unregister(oldWidget.autofillClient?.autofillId ?? autofillId);
+      _currentAutofillScope?.register(_effectiveAutofillClient);
+    }
 
     if (widget.focusNode != oldWidget.focusNode) {
       oldWidget.focusNode.removeListener(_handleFocusChanged);
@@ -1597,7 +1607,7 @@
 
     if (kIsWeb && _hasInputConnection) {
       if (oldWidget.readOnly != widget.readOnly) {
-        _textInputConnection!.updateConfig(textInputConfiguration);
+        _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration);
       }
     }
 
@@ -1980,8 +1990,9 @@
   }
 
   bool get _hasInputConnection => _textInputConnection?.attached ?? false;
-  bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false;
-  bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null;
+  /// Whether to send the autofill information to the autofill service. True by
+  /// default.
+  bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? true;
 
   void _openInputConnection() {
     if (!_shouldCreateInputConnection) {
@@ -1999,8 +2010,8 @@
       // notified to exclude this field from the autofill context. So we need to
       // provide the autofillId.
       _textInputConnection = _needsAutofill && currentAutofillScope != null
-        ? currentAutofillScope!.attach(this, textInputConfiguration)
-        : TextInput.attach(this, _createTextInputConfiguration(_isInAutofillContext || _needsAutofill));
+        ? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration)
+        : TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
       _textInputConnection!.show();
       _updateSizeAndTransform();
       _updateComposingRectIfNeeded();
@@ -2523,8 +2534,17 @@
   @override
   String get autofillId => 'EditableText-$hashCode';
 
-  TextInputConfiguration _createTextInputConfiguration(bool needsAutofillConfiguration) {
-    assert(needsAutofillConfiguration != null);
+  @override
+  TextInputConfiguration get textInputConfiguration {
+    final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
+    final AutofillConfiguration autofillConfiguration = autofillHints != null
+      ? AutofillConfiguration(
+          uniqueIdentifier: autofillId,
+          autofillHints: autofillHints,
+          currentEditingValue: currentTextEditingValue,
+        )
+      : AutofillConfiguration.disabled;
+
     return TextInputConfiguration(
       inputType: widget.keyboardType,
       readOnly: widget.readOnly,
@@ -2539,19 +2559,13 @@
       ),
       textCapitalization: widget.textCapitalization,
       keyboardAppearance: widget.keyboardAppearance,
-      autofillConfiguration: !needsAutofillConfiguration ? null : AutofillConfiguration(
-        uniqueIdentifier: autofillId,
-        autofillHints: widget.autofillHints?.toList(growable: false) ?? <String>[],
-        currentEditingValue: currentTextEditingValue,
-      ),
+      autofillConfiguration: autofillConfiguration,
       enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
     );
   }
 
   @override
-  TextInputConfiguration get textInputConfiguration {
-    return _createTextInputConfiguration(_needsAutofill);
-  }
+  void autofill(TextEditingValue value) => updateEditingValue(value);
 
   // null if no promptRect should be shown.
   TextRange? _currentPromptRectRange;
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index d353797..5af02d5 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -4776,6 +4776,22 @@
     },
   );
 
+  testWidgets('autofill info has placeholder text', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const CupertinoApp(
+        home: CupertinoTextField(
+          placeholder: 'placeholder text',
+        ),
+      ),
+    );
+    await tester.tap(find.byType(CupertinoTextField));
+
+    expect(
+      tester.testTextInput.setClientArgs?['autofill'],
+      containsPair('hintText', 'placeholder text'),
+    );
+  });
+
   testWidgets('textDirection is passed to EditableText', (WidgetTester tester) async {
     await tester.pumpWidget(
       const CupertinoApp(
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 02bc2d0..e6b0518 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -9924,4 +9924,27 @@
     expect(prefixTapCount, 1);
     expect(suffixTapCount, 1);
   });
+
+  testWidgets('autofill info has hint text', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const MaterialApp(
+        home: Material(
+          child: Center(
+            child: TextField(
+              decoration: InputDecoration(
+                hintText: 'placeholder text'
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+
+    await tester.tap(find.byType(TextField));
+
+    expect(
+      tester.testTextInput.setClientArgs?['autofill'],
+      containsPair('hintText', 'placeholder text'),
+    );
+  });
 }
diff --git a/packages/flutter/test/services/autofill_test.dart b/packages/flutter/test/services/autofill_test.dart
index 5af15da..0dfdf1d 100644
--- a/packages/flutter/test/services/autofill_test.dart
+++ b/packages/flutter/test/services/autofill_test.dart
@@ -10,7 +10,7 @@
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
-  group('TextInput message channels', () {
+  group('AutofillClient', () {
     late FakeTextChannel fakeTextChannel;
     final FakeAutofillScope scope = FakeAutofillScope();
 
@@ -25,21 +25,19 @@
       TextInput.setChannel(SystemChannels.textInput);
     });
 
-    test('throws if the hint list is empty', () async {
-      Map<String, dynamic>? json;
+    test('Does not throw if the hint list is empty', () async {
+      Object? exception;
       try {
-        const AutofillConfiguration config = AutofillConfiguration(
+        const AutofillConfiguration(
           uniqueIdentifier: 'id',
           autofillHints: <String>[],
           currentEditingValue: TextEditingValue.empty,
         );
-
-        json = config.toJson();
       } catch (e) {
-        expect(e.toString(), contains('isNotEmpty'));
+        exception = e;
       }
 
-      expect(json, isNull);
+      expect(exception, isNull);
     });
 
     test(
@@ -140,6 +138,9 @@
   void showAutocorrectionPromptRect(int start, int end) {
     latestMethodCall = 'showAutocorrectionPromptRect';
   }
+
+  @override
+  void autofill(TextEditingValue newEditingValue) => updateEditingValue(newEditingValue);
 }
 
 class FakeAutofillScope with AutofillScopeMixin implements AutofillScope {
diff --git a/packages/flutter/test/widgets/autofill_group_test.dart b/packages/flutter/test/widgets/autofill_group_test.dart
index d611745..6e7dc3f 100644
--- a/packages/flutter/test/widgets/autofill_group_test.dart
+++ b/packages/flutter/test/widgets/autofill_group_test.dart
@@ -25,7 +25,7 @@
               client1,
               AutofillGroup(
                 key: innerKey,
-                child: Column(children: const <Widget>[client2, TextField()]),
+                child: Column(children: const <Widget>[client2, TextField(autofillHints: null)]),
               ),
             ]),
           ),
@@ -36,23 +36,19 @@
     final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey));
     final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey));
 
-    final EditableTextState clientState1 = tester.state<EditableTextState>(
-      find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)),
-    );
-    final EditableTextState clientState2 = tester.state<EditableTextState>(
-      find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
-    );
+    final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
+    final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
 
-    expect(outerState.autofillClients, <EditableTextState>[clientState1]);
-    // The second TextField doesn't have autofill enabled.
-    expect(innerState.autofillClients, <EditableTextState>[clientState2]);
+    expect(outerState.autofillClients.toList(), <State<TextField>>[clientState1]);
+    // The second TextField in the AutofillGroup doesn't have autofill enabled.
+    expect(innerState.autofillClients.toList(), <State<TextField>>[clientState2]);
   });
 
   testWidgets('new clients can be added & removed to a scope', (WidgetTester tester) async {
     const Key scopeKey = Key('scope');
 
     const TextField client1 = TextField(autofillHints: <String>['1']);
-    TextField client2 = const TextField(autofillHints: <String>[]);
+    TextField client2 = const TextField(autofillHints: null);
 
     late StateSetter setState;
 
@@ -74,14 +70,10 @@
 
     final AutofillGroupState scopeState = tester.state<AutofillGroupState>(find.byKey(scopeKey));
 
-    final EditableTextState clientState1 = tester.state<EditableTextState>(
-      find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)),
-    );
-    final EditableTextState clientState2 = tester.state<EditableTextState>(
-      find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
-    );
+    final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
+    final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
 
-    expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
+    expect(scopeState.autofillClients.toList(), <State<TextField>>[clientState1]);
 
     // Add to scope.
     setState(() { client2 = const TextField(autofillHints: <String>['2']); });
@@ -93,11 +85,11 @@
     expect(scopeState.autofillClients.length, 2);
 
     // Remove from scope again.
-    setState(() { client2 = const TextField(autofillHints: <String>[]); });
+    setState(() { client2 = const TextField(autofillHints: null); });
 
     await tester.pump();
 
-    expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
+    expect(scopeState.autofillClients, <State<TextField>>[clientState1]);
   });
 
   testWidgets('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async {
@@ -131,16 +123,9 @@
     final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey));
     final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey));
 
-    final EditableTextState clientState1 = tester.state<EditableTextState>(
-      find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)),
-    );
-    final EditableTextState clientState2 = tester.state<EditableTextState>(
-      find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
-    );
-
-    final EditableTextState clientState3 = tester.state<EditableTextState>(
-      find.descendant(of: find.byKey(keyClient3), matching: find.byType(EditableText)),
-    );
+    final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
+    final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
+    final State<TextField> clientState3 = tester.state<State<TextField>>(find.byKey(keyClient3));
 
     await tester.pumpWidget(
       MaterialApp(
@@ -163,7 +148,7 @@
     expect(outerState.autofillClients.length, 2);
     expect(outerState.autofillClients, contains(clientState1));
     expect(outerState.autofillClients, contains(clientState3));
-    expect(innerState.autofillClients, <EditableTextState>[clientState2]);
+    expect(innerState.autofillClients, <State<TextField>>[clientState2]);
   });
 
   testWidgets('disposing AutofillGroups', (WidgetTester tester) async {
@@ -270,8 +255,7 @@
 
     // Remove the topmosts group group3. Should commit.
     setState(() {
-      children = const <Widget> [
-      ];
+      children = const <Widget> [];
     });
 
     await tester.pump();