autofill.dart 9.37 KB
Newer Older
1 2 3 4 5 6 7 8 9
// 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;

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
/// 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,
}

26 27
/// An [AutofillScope] widget that groups [AutofillClient]s together.
///
28 29
/// [AutofillClient]s that share the same closest [AutofillGroup] ancestor must
/// be built together, and they be will be autofilled together.
30
///
31
/// {@macro flutter.services.AutofillScope}
32 33 34 35 36
///
/// 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
37 38 39 40 41 42 43
/// 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.
///
44
/// {@macro flutter.services.TextInput.finishAutofillContext}
45 46 47 48 49 50 51 52
///
/// 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.
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 62 63 64
///
/// See also:
///
/// * [AutofillContextAction], an enum that contains predefined autofill context
///   clean up actions to be run when a topmost [AutofillGroup] is disposed.
65 66 67 68 69
class AutofillGroup extends StatefulWidget {
  /// Creates a scope for autofillable input fields.
  ///
  /// The [child] argument must not be null.
  const AutofillGroup({
70
    super.key,
71
    required this.child,
72
    this.onDisposeAction = AutofillContextAction.commit,
73
  });
74

75 76 77 78 79
  /// Returns the [AutofillGroupState] of the closest [AutofillGroup] widget
  /// which encloses the given context, or null if one cannot be found.
  ///
  /// Calling this method will create a dependency on the closest
  /// [AutofillGroup] in the [context], if there is one.
80
  ///
81
  /// {@macro flutter.widgets.AutofillGroupState}
82 83 84
  ///
  /// See also:
  ///
85 86
  /// * [AutofillGroup.of], which is similar to this method, but asserts if an
  ///   [AutofillGroup] cannot be found.
87
  /// * [EditableTextState], where this method is used to retrieve the closest
88
  ///   [AutofillGroupState].
89
  static AutofillGroupState? maybeOf(BuildContext context) {
90
    final _AutofillScope? scope = context.dependOnInheritedWidgetOfExactType<_AutofillScope>();
91 92 93
    return scope?._scope;
  }

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
  /// Returns the [AutofillGroupState] of the closest [AutofillGroup] widget
  /// which encloses the given context.
  ///
  /// If no instance is found, this method will assert in debug mode and throw
  /// an exception in release mode.
  ///
  /// Calling this method will create a dependency on the closest
  /// [AutofillGroup] in the [context].
  ///
  /// {@macro flutter.widgets.AutofillGroupState}
  ///
  /// See also:
  ///
  /// * [AutofillGroup.maybeOf], which is similar to this method, but returns
  ///   null if an [AutofillGroup] cannot be found.
  /// * [EditableTextState], where this method is used to retrieve the closest
  ///   [AutofillGroupState].
  static AutofillGroupState of(BuildContext context) {
    final AutofillGroupState? groupState = maybeOf(context);
    assert(() {
      if (groupState == null) {
        throw FlutterError(
          'AutofillGroup.of() was called with a context that does not contain an '
          'AutofillGroup widget.\n'
          'No AutofillGroup widget ancestor could be found starting from the '
          'context that was passed to AutofillGroup.of(). This can happen '
          'because you are using a widget that looks for an AutofillGroup '
          'ancestor, but no such ancestor exists.\n'
          'The context used was:\n'
          '  $context',
        );
      }
      return true;
    }());
    return groupState!;
  }

131
  /// {@macro flutter.widgets.ProxyWidget.child}
132 133
  final Widget child;

134 135 136 137
  /// 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.
  ///
138
  /// {@macro flutter.services.TextInput.finishAutofillContext}
139 140
  ///
  /// Defaults to [AutofillContextAction.commit], which prompts the platform to
141
  /// save the user input and destroy the current autofill context.
142 143
  final AutofillContextAction onDisposeAction;

144 145 146 147 148 149
  @override
  AutofillGroupState createState() => AutofillGroupState();
}

/// State associated with an [AutofillGroup] widget.
///
150
/// {@template flutter.widgets.AutofillGroupState}
151 152 153 154 155 156
/// 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).
///
157 158 159
/// 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
160
/// with the platform's text input system.
161 162 163 164 165 166
/// {@endtemplate}
///
/// Typically obtained using [AutofillGroup.of].
class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
  final Map<String, AutofillClient> _clients = <String, AutofillClient>{};

167 168 169 170 171
  // 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;

172
  @override
173
  AutofillClient? getAutofillClient(String autofillId) => _clients[autofillId];
174 175 176 177

  @override
  Iterable<AutofillClient> get autofillClients {
    return _clients.values
178
      .where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled);
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
  }

  /// 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) {
    _clients.putIfAbsent(client.autofillId, () => client);
  }

195
  /// Removes an [AutofillClient] with the given `autofillId` from this
196 197
  /// [AutofillGroup].
  ///
198 199
  /// Typically, this should be called by a text field when it's being disposed,
  /// or before it's registered with a different [AutofillGroup].
200 201 202 203 204 205 206 207 208
  ///
  /// 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) {
209
    assert(_clients.containsKey(autofillId));
210 211 212
    _clients.remove(autofillId);
  }

213 214 215
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
216
    _isTopmostAutofillGroup = AutofillGroup.maybeOf(context) == null;
217 218
  }

219 220 221 222 223 224 225
  @override
  Widget build(BuildContext context) {
    return _AutofillScope(
      autofillScopeState: this,
      child: widget.child,
    );
  }
226 227 228 229 230

  @override
  void dispose() {
    super.dispose();

231
    if (!_isTopmostAutofillGroup) {
232
      return;
233
    }
234 235 236 237 238
    switch (widget.onDisposeAction) {
      case AutofillContextAction.cancel:
        TextInput.finishAutofillContext(shouldSave: false);
        break;
      case AutofillContextAction.commit:
239
        TextInput.finishAutofillContext();
240 241 242
        break;
    }
  }
243 244 245 246
}

class _AutofillScope extends InheritedWidget {
  const _AutofillScope({
247
    required super.child,
248
    AutofillGroupState? autofillScopeState,
249
  }) : _scope = autofillScopeState;
250

251
  final AutofillGroupState? _scope;
252

253
  AutofillGroup get client => _scope!.widget;
254 255 256 257

  @override
  bool updateShouldNotify(_AutofillScope old) => _scope != old._scope;
}