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