autofill.dart 7.78 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 56
/// An example form with autofillable fields grouped into different
/// `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
  }) : assert(child != null);
74 75 76

  /// Returns the closest [AutofillGroupState] which encloses the given context.
  ///
77
  /// {@macro flutter.widgets.AutofillGroupState}
78 79 80
  ///
  /// See also:
  ///
81
  /// * [EditableTextState], where this method is used to retrieve the closest
82
  ///   [AutofillGroupState].
83 84
  static AutofillGroupState? of(BuildContext context) {
    final _AutofillScope? scope = context.dependOnInheritedWidgetOfExactType<_AutofillScope>();
85 86 87
    return scope?._scope;
  }

88
  /// {@macro flutter.widgets.ProxyWidget.child}
89 90
  final Widget child;

91 92 93 94
  /// 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.
  ///
95
  /// {@macro flutter.services.TextInput.finishAutofillContext}
96 97
  ///
  /// Defaults to [AutofillContextAction.commit], which prompts the platform to
98
  /// save the user input and destroy the current autofill context.
99 100
  final AutofillContextAction onDisposeAction;

101 102 103 104 105 106
  @override
  AutofillGroupState createState() => AutofillGroupState();
}

/// State associated with an [AutofillGroup] widget.
///
107
/// {@template flutter.widgets.AutofillGroupState}
108 109 110 111 112 113
/// 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).
///
114 115 116
/// 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
117
/// with the platform's text input system.
118 119 120 121 122 123
/// {@endtemplate}
///
/// Typically obtained using [AutofillGroup.of].
class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
  final Map<String, AutofillClient> _clients = <String, AutofillClient>{};

124 125 126 127 128
  // 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;

129
  @override
130
  AutofillClient? getAutofillClient(String autofillId) => _clients[autofillId];
131 132 133 134

  @override
  Iterable<AutofillClient> get autofillClients {
    return _clients.values
135
      .where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled);
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
  }

  /// 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);
  }

153
  /// Removes an [AutofillClient] with the given `autofillId` from this
154 155
  /// [AutofillGroup].
  ///
156 157
  /// Typically, this should be called by a text field when it's being disposed,
  /// or before it's registered with a different [AutofillGroup].
158 159 160 161 162 163 164 165 166 167 168 169 170
  ///
  /// 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);
  }

171 172 173 174 175 176
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _isTopmostAutofillGroup = AutofillGroup.of(context) == null;
  }

177 178 179 180 181 182 183
  @override
  Widget build(BuildContext context) {
    return _AutofillScope(
      autofillScopeState: this,
      child: widget.child,
    );
  }
184 185 186 187 188

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

189
    if (!_isTopmostAutofillGroup || widget.onDisposeAction == null) {
190
      return;
191
    }
192 193 194 195 196
    switch (widget.onDisposeAction) {
      case AutofillContextAction.cancel:
        TextInput.finishAutofillContext(shouldSave: false);
        break;
      case AutofillContextAction.commit:
197
        TextInput.finishAutofillContext();
198 199 200
        break;
    }
  }
201 202 203 204
}

class _AutofillScope extends InheritedWidget {
  const _AutofillScope({
205
    required super.child,
206
    AutofillGroupState? autofillScopeState,
207
  }) : _scope = autofillScopeState;
208

209
  final AutofillGroupState? _scope;
210

211
  AutofillGroup get client => _scope!.widget;
212 213 214 215

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