Skip to content

Commit 33755f2

Browse files
[autofill] opt-out instead of opt-in (#86312)
1 parent 81142c1 commit 33755f2

11 files changed

Lines changed: 260 additions & 104 deletions

File tree

packages/flutter/lib/src/cupertino/text_field.dart

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ class CupertinoTextField extends StatefulWidget {
293293
this.onTap,
294294
this.scrollController,
295295
this.scrollPhysics,
296-
this.autofillHints,
296+
this.autofillHints = const <String>[],
297297
this.restorationId,
298298
this.enableIMEPersonalizedLearning = true,
299299
}) : assert(textAlign != null),
@@ -449,7 +449,7 @@ class CupertinoTextField extends StatefulWidget {
449449
this.onTap,
450450
this.scrollController,
451451
this.scrollPhysics,
452-
this.autofillHints,
452+
this.autofillHints = const <String>[],
453453
this.restorationId,
454454
this.enableIMEPersonalizedLearning = true,
455455
}) : assert(textAlign != null),
@@ -837,7 +837,7 @@ class CupertinoTextField extends StatefulWidget {
837837
}
838838
}
839839

840-
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate {
840+
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
841841
final GlobalKey _clearGlobalKey = GlobalKey();
842842

843843
RestorableTextEditingController? _controller;
@@ -1098,6 +1098,28 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
10981098
},
10991099
);
11001100
}
1101+
// AutofillClient implementation start.
1102+
@override
1103+
String get autofillId => _editableText.autofillId;
1104+
1105+
@override
1106+
void autofill(TextEditingValue newEditingValue) => _editableText.autofill(newEditingValue);
1107+
1108+
@override
1109+
TextInputConfiguration get textInputConfiguration {
1110+
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
1111+
final AutofillConfiguration autofillConfiguration = autofillHints != null
1112+
? AutofillConfiguration(
1113+
uniqueIdentifier: autofillId,
1114+
autofillHints: autofillHints,
1115+
currentEditingValue: _effectiveController.value,
1116+
hintText: widget.placeholder,
1117+
)
1118+
: AutofillConfiguration.disabled;
1119+
1120+
return _editableText.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
1121+
}
1122+
// AutofillClient implementation end.
11011123

11021124
@override
11031125
Widget build(BuildContext context) {
@@ -1242,7 +1264,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
12421264
scrollController: widget.scrollController,
12431265
scrollPhysics: widget.scrollPhysics,
12441266
enableInteractiveSelection: widget.enableInteractiveSelection,
1245-
autofillHints: widget.autofillHints,
1267+
autofillClient: this,
12461268
restorationId: 'editable',
12471269
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
12481270
),

packages/flutter/lib/src/material/selectable_text.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
692692
enableInteractiveSelection: widget.enableInteractiveSelection,
693693
dragStartBehavior: widget.dragStartBehavior,
694694
scrollPhysics: widget.scrollPhysics,
695+
autofillHints: null,
695696
),
696697
);
697698

packages/flutter/lib/src/material/text_field.dart

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ class TextField extends StatefulWidget {
345345
this.buildCounter,
346346
this.scrollController,
347347
this.scrollPhysics,
348-
this.autofillHints,
348+
this.autofillHints = const <String>[],
349349
this.restorationId,
350350
this.enableIMEPersonalizedLearning = true,
351351
}) : assert(textAlign != null),
@@ -827,7 +827,7 @@ class TextField extends StatefulWidget {
827827
}
828828
}
829829

830-
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
830+
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
831831
RestorableTextEditingController? _controller;
832832
TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
833833

@@ -1094,6 +1094,29 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
10941094
}
10951095
}
10961096

1097+
// AutofillClient implementation start.
1098+
@override
1099+
String get autofillId => _editableText!.autofillId;
1100+
1101+
@override
1102+
void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue);
1103+
1104+
@override
1105+
TextInputConfiguration get textInputConfiguration {
1106+
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
1107+
final AutofillConfiguration autofillConfiguration = autofillHints != null
1108+
? AutofillConfiguration(
1109+
uniqueIdentifier: autofillId,
1110+
autofillHints: autofillHints,
1111+
currentEditingValue: _effectiveController.value,
1112+
hintText: (widget.decoration ?? const InputDecoration()).hintText,
1113+
)
1114+
: AutofillConfiguration.disabled;
1115+
1116+
return _editableText!.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
1117+
}
1118+
// AutofillClient implementation end.
1119+
10971120
@override
10981121
Widget build(BuildContext context) {
10991122
assert(debugCheckHasMaterial(context));
@@ -1240,7 +1263,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
12401263
dragStartBehavior: widget.dragStartBehavior,
12411264
scrollController: widget.scrollController,
12421265
scrollPhysics: widget.scrollPhysics,
1243-
autofillHints: widget.autofillHints,
1266+
autofillClient: this,
12441267
autocorrectionTextRectColor: autocorrectionTextRectColor,
12451268
restorationId: 'editable',
12461269
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,

packages/flutter/lib/src/services/autofill.dart

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -631,12 +631,40 @@ class AutofillConfiguration {
631631
/// Creates autofill related configuration information that can be sent to the
632632
/// platform.
633633
const AutofillConfiguration({
634+
required String uniqueIdentifier,
635+
required List<String> autofillHints,
636+
required TextEditingValue currentEditingValue,
637+
String? hintText,
638+
}) : this._(
639+
enabled: true,
640+
uniqueIdentifier: uniqueIdentifier,
641+
autofillHints: autofillHints,
642+
currentEditingValue: currentEditingValue,
643+
hintText: hintText,
644+
);
645+
646+
const AutofillConfiguration._({
647+
required this.enabled,
634648
required this.uniqueIdentifier,
635-
required this.autofillHints,
649+
this.autofillHints = const <String>[],
650+
this.hintText,
636651
required this.currentEditingValue,
637652
}) : assert(uniqueIdentifier != null),
638653
assert(autofillHints != null);
639654

655+
/// An [AutofillConfiguration] that indicates the [AutofillClient] does not
656+
/// wish to be autofilled.
657+
static const AutofillConfiguration disabled = AutofillConfiguration._(
658+
enabled: false,
659+
uniqueIdentifier: '',
660+
currentEditingValue: TextEditingValue.empty,
661+
);
662+
663+
/// Whether autofill should be enabled for the [AutofillClient].
664+
///
665+
/// To retrieve a disabled [AutofillConfiguration], use [disabled].
666+
final bool enabled;
667+
640668
/// A string that uniquely identifies the current [AutofillClient].
641669
///
642670
/// The identifier needs to be unique within the [AutofillScope] for the
@@ -648,7 +676,7 @@ class AutofillConfiguration {
648676
/// A list of strings that helps the autofill service identify the type of the
649677
/// [AutofillClient].
650678
///
651-
/// Must not be null or empty.
679+
/// Must not be null.
652680
///
653681
/// {@template flutter.services.AutofillConfiguration.autofillHints}
654682
/// For the best results, hint strings need to be understood by the platform's
@@ -697,14 +725,23 @@ class AutofillConfiguration {
697725
/// The current [TextEditingValue] of the [AutofillClient].
698726
final TextEditingValue currentEditingValue;
699727

728+
/// The optional hint text placed on the view that typically suggests what
729+
/// sort of input the field accepts, for example "enter your password here".
730+
///
731+
/// If the developer does not specify any [autofillHints], the [hintText] can
732+
/// be a useful indication to the platform autofill service.
733+
final String? hintText;
734+
700735
/// Returns a representation of this object as a JSON object.
701-
Map<String, dynamic> toJson() {
702-
assert(autofillHints.isNotEmpty);
703-
return <String, dynamic>{
704-
'uniqueIdentifier': uniqueIdentifier,
705-
'hints': autofillHints,
706-
'editingValue': currentEditingValue.toJSON(),
707-
};
736+
Map<String, dynamic>? toJson() {
737+
return enabled
738+
? <String, dynamic>{
739+
'uniqueIdentifier': uniqueIdentifier,
740+
'hints': autofillHints,
741+
'editingValue': currentEditingValue.toJSON(),
742+
if (hintText != null) 'hintText': hintText,
743+
}
744+
: null;
708745
}
709746
}
710747

@@ -715,7 +752,7 @@ class AutofillConfiguration {
715752
abstract class AutofillClient {
716753
/// The unique identifier of this [AutofillClient].
717754
///
718-
/// Must not be null.
755+
/// Must not be null and the identifier must not be changed.
719756
String get autofillId;
720757

721758
/// The [TextInputConfiguration] that describes this [AutofillClient].
@@ -726,7 +763,7 @@ abstract class AutofillClient {
726763

727764
/// Requests this [AutofillClient] update its [TextEditingValue] to the given
728765
/// value.
729-
void updateEditingValue(TextEditingValue newEditingValue);
766+
void autofill(TextEditingValue newEditingValue);
730767
}
731768

732769
/// An ordered group within which [AutofillClient]s are logically connected.
@@ -806,7 +843,7 @@ mixin AutofillScopeMixin implements AutofillScope {
806843
TextInputConnection attach(TextInputClient trigger, TextInputConfiguration configuration) {
807844
assert(trigger != null);
808845
assert(
809-
!autofillClients.any((AutofillClient client) => client.textInputConfiguration.autofillConfiguration == null),
846+
!autofillClients.any((AutofillClient client) => !client.textInputConfiguration.autofillConfiguration.enabled),
810847
'Every client in AutofillScope.autofillClients must enable autofill',
811848
);
812849

packages/flutter/lib/src/services/text_input.dart

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ class TextInputConfiguration {
466466
this.inputAction = TextInputAction.done,
467467
this.keyboardAppearance = Brightness.light,
468468
this.textCapitalization = TextCapitalization.none,
469-
this.autofillConfiguration,
469+
this.autofillConfiguration = AutofillConfiguration.disabled,
470470
this.enableIMEPersonalizedLearning = true,
471471
}) : assert(inputType != null),
472472
assert(obscureText != null),
@@ -503,7 +503,7 @@ class TextInputConfiguration {
503503
/// to the platform. This will prevent the corresponding input field from
504504
/// participating in autofills triggered by other fields. Additionally, on
505505
/// Android and web, setting [autofillConfiguration] to null disables autofill.
506-
final AutofillConfiguration? autofillConfiguration;
506+
final AutofillConfiguration autofillConfiguration;
507507

508508
/// {@template flutter.services.TextInputConfiguration.smartDashesType}
509509
/// Whether to allow the platform to automatically format dashes.
@@ -607,8 +607,41 @@ class TextInputConfiguration {
607607
/// {@endtemplate}
608608
final bool enableIMEPersonalizedLearning;
609609

610+
/// Creates a copy of this [TextInputConfiguration] with the given fields
611+
/// replaced with new values.
612+
TextInputConfiguration copyWith({
613+
TextInputType? inputType,
614+
bool? readOnly,
615+
bool? obscureText,
616+
bool? autocorrect,
617+
SmartDashesType? smartDashesType,
618+
SmartQuotesType? smartQuotesType,
619+
bool? enableSuggestions,
620+
String? actionLabel,
621+
TextInputAction? inputAction,
622+
Brightness? keyboardAppearance,
623+
TextCapitalization? textCapitalization,
624+
bool? enableIMEPersonalizedLearning,
625+
AutofillConfiguration? autofillConfiguration,
626+
}) {
627+
return TextInputConfiguration(
628+
inputType: inputType ?? this.inputType,
629+
readOnly: readOnly ?? this.readOnly,
630+
obscureText: obscureText ?? this.obscureText,
631+
autocorrect: autocorrect ?? this.autocorrect,
632+
smartDashesType: smartDashesType ?? this.smartDashesType,
633+
smartQuotesType: smartQuotesType ?? this.smartQuotesType,
634+
enableSuggestions: enableSuggestions ?? this.enableSuggestions,
635+
inputAction: inputAction ?? this.inputAction,
636+
textCapitalization: textCapitalization ?? this.textCapitalization,
637+
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
638+
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
639+
autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
640+
);
641+
}
610642
/// Returns a representation of this object as a JSON object.
611643
Map<String, dynamic> toJson() {
644+
final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
612645
return <String, dynamic>{
613646
'inputType': inputType.toJson(),
614647
'readOnly': readOnly,
@@ -622,7 +655,7 @@ class TextInputConfiguration {
622655
'textCapitalization': textCapitalization.toString(),
623656
'keyboardAppearance': keyboardAppearance.toString(),
624657
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
625-
if (autofillConfiguration != null) 'autofill': autofillConfiguration!.toJson(),
658+
if (autofill != null) 'autofill': autofill,
626659
};
627660
}
628661
}
@@ -1470,7 +1503,10 @@ class TextInput {
14701503
final TextEditingValue textEditingValue = TextEditingValue.fromJSON(
14711504
editingValue[tag] as Map<String, dynamic>,
14721505
);
1473-
scope?.getAutofillClient(tag)?.updateEditingValue(textEditingValue);
1506+
final AutofillClient? client = scope?.getAutofillClient(tag);
1507+
if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) {
1508+
client.autofill(textEditingValue);
1509+
}
14741510
}
14751511

14761512
return;

packages/flutter/lib/src/widgets/autofill.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
134134
@override
135135
Iterable<AutofillClient> get autofillClients {
136136
return _clients.values
137-
.where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration != null);
137+
.where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled);
138138
}
139139

140140
/// Adds the [AutofillClient] to this [AutofillGroup].
@@ -155,9 +155,8 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
155155
/// Removes an [AutofillClient] with the given `autofillId` from this
156156
/// [AutofillGroup].
157157
///
158-
/// Typically, this should be called by autofillable [TextInputClient]s in
159-
/// [State.dispose] and [State.didChangeDependencies], when the input field
160-
/// needs to be removed from the [AutofillGroup] it is currently registered to.
158+
/// Typically, this should be called by a text field when it's being disposed,
159+
/// or before it's registered with a different [AutofillGroup].
161160
///
162161
/// See also:
163162
///

0 commit comments

Comments
 (0)