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';
8library;
9
10import 'package:flutter/services.dart';
11import 'framework.dart';
12
13export 'package:flutter/services.dart' show AutofillHints;
14
15/// Predefined autofill context clean up actions.
16enum 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.
70class 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].
167class 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
247class _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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com