| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'editable_text.dart'; |
| 6 | /// @docImport 'form.dart'; |
| 7 | /// @docImport 'scrollable.dart'; |
| 8 | library; |
| 9 | |
| 10 | import 'package:flutter/services.dart'; |
| 11 | import 'framework.dart'; |
| 12 | |
| 13 | export 'package:flutter/services.dart' show AutofillHints; |
| 14 | |
| 15 | /// Predefined autofill context clean up actions. |
| 16 | enum AutofillContextAction { |
| 17 | /// Destroys the current autofill context after informing the platform to save |
| 18 | /// the user input from it. |
| 19 | /// |
| 20 | /// Corresponds to calling [TextInput.finishAutofillContext] with |
| 21 | /// `shouldSave == true`. |
| 22 | commit, |
| 23 | |
| 24 | /// Destroys the current autofill context without saving the user input. |
| 25 | /// |
| 26 | /// Corresponds to calling [TextInput.finishAutofillContext] with |
| 27 | /// `shouldSave == false`. |
| 28 | cancel, |
| 29 | } |
| 30 | |
| 31 | /// An [AutofillScope] widget that groups [AutofillClient]s together. |
| 32 | /// |
| 33 | /// [AutofillClient]s that share the same closest [AutofillGroup] ancestor must |
| 34 | /// be built together, and they will be autofilled together. |
| 35 | /// |
| 36 | /// {@macro flutter.services.AutofillScope} |
| 37 | /// |
| 38 | /// The [AutofillGroup] widget only knows about [AutofillClient]s registered to |
| 39 | /// it using the [AutofillGroupState.register] API. Typically, [AutofillGroup] |
| 40 | /// will not pick up [AutofillClient]s that are not mounted, for example, an |
| 41 | /// [AutofillClient] within a [Scrollable] that has never been scrolled into the |
| 42 | /// viewport. To workaround this problem, ensure clients in the same |
| 43 | /// [AutofillGroup] are built together. |
| 44 | /// |
| 45 | /// The topmost [AutofillGroup] widgets (the ones that are closest to the root |
| 46 | /// widget) can be used to clean up the current autofill context when the |
| 47 | /// current autofill context is no longer relevant. |
| 48 | /// |
| 49 | /// {@macro flutter.services.TextInput.finishAutofillContext} |
| 50 | /// |
| 51 | /// By default, [onDisposeAction] is set to [AutofillContextAction.commit], in |
| 52 | /// which case when any of the topmost [AutofillGroup]s is being disposed, the |
| 53 | /// platform will be informed to save the user input from the current autofill |
| 54 | /// context, then the current autofill context will be destroyed, to free |
| 55 | /// resources. You can, for example, wrap a route that contains a [Form] full of |
| 56 | /// autofillable input fields in an [AutofillGroup], so the user input of the |
| 57 | /// [Form] can be saved for future autofill by the platform. |
| 58 | /// |
| 59 | /// {@tool dartpad} |
| 60 | /// An example form with autofillable fields grouped into different |
| 61 | /// [AutofillGroup]s. |
| 62 | /// |
| 63 | /// ** See code in examples/api/lib/widgets/autofill/autofill_group.0.dart ** |
| 64 | /// {@end-tool} |
| 65 | /// |
| 66 | /// See also: |
| 67 | /// |
| 68 | /// * [AutofillContextAction], an enum that contains predefined autofill context |
| 69 | /// clean up actions to be run when a topmost [AutofillGroup] is disposed. |
| 70 | class AutofillGroup extends StatefulWidget { |
| 71 | /// Creates a scope for autofillable input fields. |
| 72 | const AutofillGroup({ |
| 73 | super.key, |
| 74 | required this.child, |
| 75 | this.onDisposeAction = AutofillContextAction.commit, |
| 76 | }); |
| 77 | |
| 78 | /// Returns the [AutofillGroupState] of the closest [AutofillGroup] widget |
| 79 | /// which encloses the given context, or null if one cannot be found. |
| 80 | /// |
| 81 | /// Calling this method will create a dependency on the closest |
| 82 | /// [AutofillGroup] in the [context], if there is one. |
| 83 | /// |
| 84 | /// {@macro flutter.widgets.AutofillGroupState} |
| 85 | /// |
| 86 | /// See also: |
| 87 | /// |
| 88 | /// * [AutofillGroup.of], which is similar to this method, but asserts if an |
| 89 | /// [AutofillGroup] cannot be found. |
| 90 | /// * [EditableTextState], where this method is used to retrieve the closest |
| 91 | /// [AutofillGroupState]. |
| 92 | static AutofillGroupState? maybeOf(BuildContext context) { |
| 93 | final _AutofillScope? scope = context.dependOnInheritedWidgetOfExactType<_AutofillScope>(); |
| 94 | return scope?._scope; |
| 95 | } |
| 96 | |
| 97 | /// Returns the [AutofillGroupState] of the closest [AutofillGroup] widget |
| 98 | /// which encloses the given context. |
| 99 | /// |
| 100 | /// If no instance is found, this method will assert in debug mode and throw |
| 101 | /// an exception in release mode. |
| 102 | /// |
| 103 | /// Calling this method will create a dependency on the closest |
| 104 | /// [AutofillGroup] in the [context]. |
| 105 | /// |
| 106 | /// {@macro flutter.widgets.AutofillGroupState} |
| 107 | /// |
| 108 | /// See also: |
| 109 | /// |
| 110 | /// * [AutofillGroup.maybeOf], which is similar to this method, but returns |
| 111 | /// null if an [AutofillGroup] cannot be found. |
| 112 | /// * [EditableTextState], where this method is used to retrieve the closest |
| 113 | /// [AutofillGroupState]. |
| 114 | static AutofillGroupState of(BuildContext context) { |
| 115 | final AutofillGroupState? groupState = maybeOf(context); |
| 116 | assert(() { |
| 117 | if (groupState == null) { |
| 118 | throw FlutterError( |
| 119 | 'AutofillGroup.of() was called with a context that does not contain an ' |
| 120 | 'AutofillGroup widget.\n' |
| 121 | 'No AutofillGroup widget ancestor could be found starting from the ' |
| 122 | 'context that was passed to AutofillGroup.of(). This can happen ' |
| 123 | 'because you are using a widget that looks for an AutofillGroup ' |
| 124 | 'ancestor, but no such ancestor exists.\n' |
| 125 | 'The context used was:\n' |
| 126 | ' $context' , |
| 127 | ); |
| 128 | } |
| 129 | return true; |
| 130 | }()); |
| 131 | return groupState!; |
| 132 | } |
| 133 | |
| 134 | /// {@macro flutter.widgets.ProxyWidget.child} |
| 135 | final Widget child; |
| 136 | |
| 137 | /// The [AutofillContextAction] to be run when this [AutofillGroup] is the |
| 138 | /// topmost [AutofillGroup] and it's being disposed, in order to clean up the |
| 139 | /// current autofill context. |
| 140 | /// |
| 141 | /// {@macro flutter.services.TextInput.finishAutofillContext} |
| 142 | /// |
| 143 | /// Defaults to [AutofillContextAction.commit], which prompts the platform to |
| 144 | /// save the user input and destroy the current autofill context. |
| 145 | final AutofillContextAction onDisposeAction; |
| 146 | |
| 147 | @override |
| 148 | AutofillGroupState createState() => AutofillGroupState(); |
| 149 | } |
| 150 | |
| 151 | /// State associated with an [AutofillGroup] widget. |
| 152 | /// |
| 153 | /// {@template flutter.widgets.AutofillGroupState} |
| 154 | /// An [AutofillGroupState] can be used to register an [AutofillClient] when it |
| 155 | /// enters this [AutofillGroup] (for example, when an [EditableText] is mounted or |
| 156 | /// reparented onto the [AutofillGroup]'s subtree), and unregister an |
| 157 | /// [AutofillClient] when it exits (for example, when an [EditableText] gets |
| 158 | /// unmounted or reparented out of the [AutofillGroup]'s subtree). |
| 159 | /// |
| 160 | /// The [AutofillGroupState] class also provides an [AutofillGroupState.attach] |
| 161 | /// method that can be called by [TextInputClient]s that support autofill, |
| 162 | /// instead of [TextInput.attach], to create a [TextInputConnection] to interact |
| 163 | /// with the platform's text input system. |
| 164 | /// {@endtemplate} |
| 165 | /// |
| 166 | /// Typically obtained using [AutofillGroup.of]. |
| 167 | class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin { |
| 168 | final Map<String, AutofillClient> _clients = <String, AutofillClient>{}; |
| 169 | |
| 170 | // Whether this AutofillGroup widget is the topmost AutofillGroup (i.e., it |
| 171 | // has no AutofillGroup ancestor). Each topmost AutofillGroup runs its |
| 172 | // `AutofillGroup.onDisposeAction` when it gets disposed. |
| 173 | bool _isTopmostAutofillGroup = false; |
| 174 | |
| 175 | @override |
| 176 | AutofillClient? getAutofillClient(String autofillId) => _clients[autofillId]; |
| 177 | |
| 178 | @override |
| 179 | Iterable<AutofillClient> get autofillClients { |
| 180 | return _clients.values.where( |
| 181 | (AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled, |
| 182 | ); |
| 183 | } |
| 184 | |
| 185 | /// Adds the [AutofillClient] to this [AutofillGroup]. |
| 186 | /// |
| 187 | /// Typically, this is called by [TextInputClient]s that support autofill (for |
| 188 | /// example, [EditableTextState]) in [State.didChangeDependencies], when the |
| 189 | /// input field should be registered to a new [AutofillGroup]. |
| 190 | /// |
| 191 | /// See also: |
| 192 | /// |
| 193 | /// * [EditableTextState.didChangeDependencies], where this method is called |
| 194 | /// to update the current [AutofillScope] when needed. |
| 195 | void register(AutofillClient client) { |
| 196 | _clients.putIfAbsent(client.autofillId, () => client); |
| 197 | } |
| 198 | |
| 199 | /// Removes an [AutofillClient] with the given `autofillId` from this |
| 200 | /// [AutofillGroup]. |
| 201 | /// |
| 202 | /// Typically, this should be called by a text field when it's being disposed, |
| 203 | /// or before it's registered with a different [AutofillGroup]. |
| 204 | /// |
| 205 | /// See also: |
| 206 | /// |
| 207 | /// * [EditableTextState.didChangeDependencies], where this method is called |
| 208 | /// to unregister from the previous [AutofillScope]. |
| 209 | /// * [EditableTextState.dispose], where this method is called to unregister |
| 210 | /// from the current [AutofillScope] when the widget is about to be removed |
| 211 | /// from the tree. |
| 212 | void unregister(String autofillId) { |
| 213 | assert(_clients.containsKey(autofillId)); |
| 214 | _clients.remove(autofillId); |
| 215 | } |
| 216 | |
| 217 | @protected |
| 218 | @override |
| 219 | void didChangeDependencies() { |
| 220 | super.didChangeDependencies(); |
| 221 | _isTopmostAutofillGroup = AutofillGroup.maybeOf(context) == null; |
| 222 | } |
| 223 | |
| 224 | @protected |
| 225 | @override |
| 226 | Widget build(BuildContext context) { |
| 227 | return _AutofillScope(autofillScopeState: this, child: widget.child); |
| 228 | } |
| 229 | |
| 230 | @protected |
| 231 | @override |
| 232 | void dispose() { |
| 233 | super.dispose(); |
| 234 | |
| 235 | if (!_isTopmostAutofillGroup) { |
| 236 | return; |
| 237 | } |
| 238 | switch (widget.onDisposeAction) { |
| 239 | case AutofillContextAction.cancel: |
| 240 | TextInput.finishAutofillContext(shouldSave: false); |
| 241 | case AutofillContextAction.commit: |
| 242 | TextInput.finishAutofillContext(); |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | class _AutofillScope extends InheritedWidget { |
| 248 | const _AutofillScope({required super.child, AutofillGroupState? autofillScopeState}) |
| 249 | : _scope = autofillScopeState; |
| 250 | |
| 251 | final AutofillGroupState? _scope; |
| 252 | |
| 253 | AutofillGroup get client => _scope!.widget; |
| 254 | |
| 255 | @override |
| 256 | bool updateShouldNotify(_AutofillScope old) => _scope != old._scope; |
| 257 | } |
| 258 | |