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 | |