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