-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Autofill save #58731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Autofill save #58731
Changes from all commits
7ffc9a8
7a3f28d
4810169
14b5dc9
6cc2e62
73542a9
a74c72d
812ff35
0be1b74
2d92adf
1934cfc
88a3023
04b5ab7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -850,12 +850,14 @@ class TextInputConnection { | |
| TextInput._instance._show(); | ||
| } | ||
|
|
||
| /// Requests the platform autofill UI to appear. | ||
| /// Requests the system autofill UI to appear. | ||
| /// | ||
| /// The call has no effect unless the currently attached client supports | ||
| /// autofill, and the platform has a standalone autofill UI (for example, this | ||
| /// call has no effect on iOS since its autofill UI is part of the software | ||
| /// keyboard). | ||
| /// Currently only works on Android. Other platforms do not respond to this | ||
| /// message. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [EditableText], a [TextInputClient] that calls this method when focused. | ||
| void requestAutofill() { | ||
| assert(attached); | ||
| TextInput._instance._requestAutofill(); | ||
|
|
@@ -1213,4 +1215,58 @@ class TextInput { | |
| args, | ||
| ); | ||
| } | ||
|
|
||
| /// Finishes the current autofill context, and potentially saves the user | ||
| /// input for future use if `shouldSave` is true. | ||
| /// | ||
| /// Typically, this method should be called when the user has finalized their | ||
| /// input. For example, in a [Form], it's typically done immediately before or | ||
| /// after its content is submitted. | ||
| /// | ||
| /// The topmost [AutofillGroup]s also call [finishAutofillContext] | ||
| /// automatically when they are disposed. The default behavior can be | ||
| /// overridden in [AutofillGroup.onDisposeAction]. | ||
| /// | ||
| /// {@template flutter.services.autofill.autofillContext} | ||
| /// An autofill context is a collection of input fields that live in the | ||
| /// platform's text input plugin. The platform is encouraged to save the user | ||
| /// input stored in the current autofill context before the context is | ||
| /// destroyed, when [finishAutofillContext] is called with `shouldSave` set to | ||
| /// true. | ||
| /// | ||
| /// Currently, there can only be at most one autofill context at any given | ||
| /// time. When any input field in an [AutofillGroup] requests for autofill | ||
| /// (which is done automatically when an autofillable [EditableText] gains | ||
| /// focus), the current autofill context will merge the content of that | ||
| /// [AutofillGroup] into itself. When there isn't an existing autofill context, | ||
| /// one will be created to hold the newly added input fields from the group. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Since we have only one
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
| /// | ||
| /// Once added to an autofill context, an input field will stay in the context | ||
| /// until the context is destroyed. To prevent leaks, call [finishAutofillContext] | ||
| /// to signal the text input plugin that the user has finalized their input in | ||
| /// the current autofill context. The platform text input plugin either | ||
| /// encourages or discourages the platform from saving the user input based on | ||
| /// the value of the `shouldSave` parameter. The platform usually shows a | ||
| /// "Save for autofill?" prompt for user confirmation. | ||
| /// {@endtemplate} | ||
| /// | ||
| /// On many platforms, calling [finishAutofillContext] shows the save user | ||
| /// input dialog and disrupts the user's flow. Ideally the dialog should only | ||
| /// be shown no more than once for every screen. Consider removing premature | ||
| /// [finishAutofillContext] calls to prevent showing the save user input UI | ||
| /// too frequently. However, calling [finishAutofillContext] when there's no | ||
| /// existing autofill context usually does not bring up the save user input | ||
| /// UI. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [AutofillGroup.onDisposeAction], a configurable action that runs when a | ||
| /// topmost [AutofillGroup] is getting disposed. | ||
| static void finishAutofillContext({ bool shouldSave = true }) { | ||
| assert(shouldSave != null); | ||
| TextInput._instance._channel.invokeMethod<void>( | ||
| 'TextInput.finishAutofillContext', | ||
| shouldSave , | ||
| ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also change this method to send "commit/cancel" information as an argument to make it similar to the other methods?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update to take a bool. Let me update the engine PR. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,23 +9,54 @@ import 'framework.dart'; | |
|
|
||
| export 'package:flutter/services.dart' show AutofillHints; | ||
|
|
||
| /// Predefined autofill context clean up actions. | ||
| enum AutofillContextAction { | ||
| /// Destroys the current autofill context after informing the platform to save | ||
| /// the user input from it. | ||
| /// | ||
| /// Corresponds to calling [TextInput.finishAutofillContext] with | ||
| /// `shouldSave == true`. | ||
| commit, | ||
|
|
||
| /// Destroys the current autofill context without saving the user input. | ||
| /// | ||
| /// Corresponds to calling [TextInput.finishAutofillContext] with | ||
| /// `shouldSave == false`. | ||
| cancel, | ||
| } | ||
|
|
||
| /// An [AutofillScope] widget that groups [AutofillClient]s together. | ||
| /// | ||
| /// [AutofillClient]s within the same [AutofillScope] must be built together, and | ||
| /// they be will be autofilled together. | ||
| /// [AutofillClient]s that share the same closest [AutofillGroup] ancestor must | ||
| /// be built together, and they be will be autofilled together. | ||
| /// | ||
| /// {@macro flutter.services.autofill.AutofillScope} | ||
| /// | ||
| /// The [AutofillGroup] widget only knows about [AutofillClient]s registered to | ||
| /// it using the [AutofillGroupState.register] API. Typically, [AutofillGroup] | ||
| /// will not pick up [AutofillClient]s that are not mounted, for example, an | ||
| /// [AutofillClient] within a [Scrollable] that has never been scrolled into the | ||
| /// viewport. To workaround this problem, ensure clients in the same [AutofillGroup] | ||
| /// are built together: | ||
| /// viewport. To workaround this problem, ensure clients in the same | ||
| /// [AutofillGroup] are built together. | ||
| /// | ||
| /// The topmost [AutofillGroup] widgets (the ones that are closest to the root | ||
| /// widget) can be used to clean up the current autofill context when the | ||
| /// current autofill context is no longer relevant. | ||
| /// | ||
| /// {@macro flutter.services.autofill.autofillContext} | ||
| /// | ||
| /// By default, [onDisposeAction] is set to [AutofillContextAction.commit], in | ||
| /// which case when any of the topmost [AutofillGroup]s is being disposed, the | ||
| /// platform will be informed to save the user input from the current autofill | ||
| /// context, then the current autofill context will be destroyed, to free | ||
| /// resources. You can, for example, wrap a route that contains a [Form] full of | ||
| /// autofillable input fields in an [AutofillGroup], so the user input of the | ||
| /// [Form] can be saved for future autofill by the platform. | ||
| /// | ||
| /// {@tool dartpad --template=stateful_widget_scaffold} | ||
| /// | ||
| /// An example form with autofillable fields grouped into different `AutofillGroup`s. | ||
| /// An example form with autofillable fields grouped into different | ||
| /// `AutofillGroup`s. | ||
| /// | ||
| /// ```dart | ||
| /// bool isSameAddress = true; | ||
|
|
@@ -44,8 +75,8 @@ export 'package:flutter/services.dart' show AutofillHints; | |
| /// return ListView( | ||
| /// children: <Widget>[ | ||
| /// const Text('Shipping address'), | ||
| /// // The address fields are grouped together as some platforms are capable | ||
| /// // of autofilling all these fields in one go. | ||
| /// // The address fields are grouped together as some platforms are | ||
| /// // capable of autofilling all of these fields in one go. | ||
| /// AutofillGroup( | ||
| /// child: Column( | ||
| /// children: <Widget>[ | ||
|
|
@@ -83,8 +114,8 @@ export 'package:flutter/services.dart' show AutofillHints; | |
| /// ), | ||
| /// ), | ||
| /// const Text('Credit Card Information'), | ||
| /// // The credit card number and the security code are grouped together as | ||
| /// // some platforms are capable of autofilling both fields. | ||
| /// // The credit card number and the security code are grouped together | ||
| /// // as some platforms are capable of autofilling both fields. | ||
| /// AutofillGroup( | ||
| /// child: Column( | ||
| /// children: <Widget>[ | ||
|
|
@@ -111,13 +142,19 @@ export 'package:flutter/services.dart' show AutofillHints; | |
| /// } | ||
| /// ``` | ||
| /// {@end-tool} | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [AutofillContextAction], an enum that contains predefined autofill context | ||
| /// clean up actions to be run when a topmost [AutofillGroup] is disposed. | ||
| class AutofillGroup extends StatefulWidget { | ||
| /// Creates a scope for autofillable input fields. | ||
| /// | ||
| /// The [child] argument must not be null. | ||
| const AutofillGroup({ | ||
| Key key, | ||
| @required this.child, | ||
| this.onDisposeAction = AutofillContextAction.commit, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. assert that this is not null?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's nullable, and on dispose action is going to be a no-op when it's null.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should document what no-op in that case means (and instead of using null, can we define a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder why is a separate enum case preferred over |
||
| }) : assert(child != null), | ||
| super(key: key); | ||
|
|
||
|
|
@@ -137,6 +174,17 @@ class AutofillGroup extends StatefulWidget { | |
| /// {@macro flutter.widgets.child} | ||
| final Widget child; | ||
|
|
||
| /// The [AutofillContextAction] to be run when this [AutofillGroup] is the | ||
| /// topmost [AutofillGroup] and it's being disposed, in order to clean up the | ||
| /// current autofill context. | ||
| /// | ||
| /// {@macro flutter.services.autofill.autofillContext} | ||
| /// | ||
| /// Defaults to [AutofillContextAction.commit], which prompts the platform to | ||
| /// save the user input and destroy the current autofill context. No action | ||
| /// will be taken if [onDisposeAction] is set to null. | ||
| final AutofillContextAction onDisposeAction; | ||
|
|
||
| @override | ||
| AutofillGroupState createState() => AutofillGroupState(); | ||
| } | ||
|
|
@@ -160,6 +208,11 @@ class AutofillGroup extends StatefulWidget { | |
| class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin { | ||
| final Map<String, AutofillClient> _clients = <String, AutofillClient>{}; | ||
|
|
||
| // Whether this AutofillGroup widget is the topmost AutofillGroup (i.e., it | ||
| // has no AutofillGroup ancestor). Each topmost AutofillGroup runs its | ||
| // `AutofillGroup.onDisposeAction` when it gets disposed. | ||
| bool _isTopmostAutofillGroup = false; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The definition on topmost (the ones that are closest to the root widget) is in the comments on line 54. It is not very close to this variable. In order to increase readability of the code, and make it easier to understand by developers checking this variable, let's document
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment in the code. |
||
|
|
||
| @override | ||
| AutofillClient getAutofillClient(String tag) => _clients[tag]; | ||
|
|
||
|
|
@@ -184,7 +237,7 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin { | |
| _clients.putIfAbsent(client.autofillId, () => client); | ||
| } | ||
|
|
||
| /// Removes an [AutofillClient] with the given [autofillId] from this | ||
| /// Removes an [AutofillClient] with the given `autofillId` from this | ||
| /// [AutofillGroup]. | ||
| /// | ||
| /// Typically, this should be called by autofillable [TextInputClient]s in | ||
|
|
@@ -203,13 +256,35 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin { | |
| _clients.remove(autofillId); | ||
| } | ||
|
|
||
| @override | ||
| void didChangeDependencies() { | ||
| super.didChangeDependencies(); | ||
| _isTopmostAutofillGroup = AutofillGroup.of(context) == null; | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return _AutofillScope( | ||
| autofillScopeState: this, | ||
| child: widget.child, | ||
| ); | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| super.dispose(); | ||
|
|
||
| if (!_isTopmostAutofillGroup || widget.onDisposeAction == null) | ||
| return; | ||
| switch (widget.onDisposeAction) { | ||
| case AutofillContextAction.cancel: | ||
| TextInput.finishAutofillContext(shouldSave: false); | ||
| break; | ||
| case AutofillContextAction.commit: | ||
| TextInput.finishAutofillContext(shouldSave: true); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| class _AutofillScope extends InheritedWidget { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1344,6 +1344,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
| @override | ||
| AutofillScope get currentAutofillScope => _currentAutofillScope; | ||
|
|
||
| // Is this field in the current autofill context. | ||
| bool _isInAutofillContext = false; | ||
|
|
||
| // This value is an eyeball estimation of the time it takes for the iOS cursor | ||
| // to ease in and out. | ||
| static const Duration _fadeDuration = Duration(milliseconds: 250); | ||
|
|
@@ -1404,6 +1407,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
| _currentAutofillScope?.unregister(autofillId); | ||
| _currentAutofillScope = newAutofillGroup; | ||
| newAutofillGroup?.register(this); | ||
| _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _isInAutofillContext can never be set to false once it becomes true, is that right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. If a text field's was in the current autofill context, and then its autofill gets disabled (by setting |
||
| } | ||
|
|
||
| if (!_didAutoFocus && widget.autofocus) { | ||
|
|
@@ -1428,6 +1432,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
| _selectionOverlay?.update(_value); | ||
| } | ||
| _selectionOverlay?.handlesVisible = widget.showSelectionHandles; | ||
| _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext; | ||
|
|
||
| if (widget.focusNode != oldWidget.focusNode) { | ||
| oldWidget.focusNode.removeListener(_handleFocusChanged); | ||
| _focusAttachment?.detach(); | ||
|
|
@@ -1710,6 +1716,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
| } | ||
|
|
||
| bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached; | ||
| bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false; | ||
| bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null; | ||
|
|
||
| void _openInputConnection() { | ||
| if (widget.readOnly) { | ||
|
|
@@ -1719,14 +1727,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
| final TextEditingValue localValue = _value; | ||
| _lastFormattedUnmodifiedTextEditingValue = localValue; | ||
|
|
||
| _textInputConnection = (widget.autofillHints?.isNotEmpty ?? false) && currentAutofillScope != null | ||
| // When _needsAutofill == true && currentAutofillScope == null, autofill | ||
| // is allowed but saving the user input from the text field is | ||
| // discouraged. | ||
| // | ||
| // In case the autofillScope changes from a non-null value to null, or | ||
| // _needsAutofill changes to false from true, the platform needs to be | ||
| // 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, textInputConfiguration); | ||
| : TextInput.attach(this, _createTextInputConfiguration(_isInAutofillContext || _needsAutofill)); | ||
| _textInputConnection.show(); | ||
| _updateSizeAndTransform(); | ||
| // Request autofill AFTER the size and the transform have been sent to the | ||
| // platform side. | ||
| _textInputConnection.requestAutofill(); | ||
| if (_needsAutofill) { | ||
| // Request autofill AFTER the size and the transform have been sent to | ||
| // the platform text input plugin. | ||
| _textInputConnection.requestAutofill(); | ||
| } | ||
|
|
||
| final TextStyle style = widget.style; | ||
| _textInputConnection | ||
|
|
@@ -2157,9 +2175,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
| @override | ||
| String get autofillId => 'EditableText-$hashCode'; | ||
|
|
||
| @override | ||
| TextInputConfiguration get textInputConfiguration { | ||
| final bool isAutofillEnabled = widget.autofillHints?.isNotEmpty ?? false; | ||
| TextInputConfiguration _createTextInputConfiguration(bool needsAutofillConfiguration) { | ||
| assert(needsAutofillConfiguration != null); | ||
| return TextInputConfiguration( | ||
| inputType: widget.keyboardType, | ||
| obscureText: widget.obscureText, | ||
|
|
@@ -2173,14 +2190,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
| ), | ||
| textCapitalization: widget.textCapitalization, | ||
| keyboardAppearance: widget.keyboardAppearance, | ||
| autofillConfiguration: !isAutofillEnabled ? null : AutofillConfiguration( | ||
| autofillConfiguration: !needsAutofillConfiguration ? null : AutofillConfiguration( | ||
| uniqueIdentifier: autofillId, | ||
| autofillHints: widget.autofillHints.toList(growable: false), | ||
| autofillHints: widget.autofillHints?.toList(growable: false) ?? <String>[], | ||
| currentEditingValue: currentTextEditingValue, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| @override | ||
| TextInputConfiguration get textInputConfiguration { | ||
| return _createTextInputConfiguration(_needsAutofill); | ||
| } | ||
|
|
||
| // null if no promptRect should be shown. | ||
| TextRange _currentPromptRectRange; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a rather long and informative comment. It might help to briefly summarize how a typical app will use finishAutofillContext, before going deep. The paragraph at the end of this would serve the purpose.