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
5import 'package:flutter/services.dart';
6import 'framework.dart';
7
8export 'package:flutter/services.dart' show AutofillHints;
9
10/// Predefined autofill context clean up actions.
11enum 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.
65class 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].
162class 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
241class _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