autofill.dart 7.85 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 71
    Key? key,
    required this.child,
72
    this.onDisposeAction = AutofillContextAction.commit,
73 74 75 76 77
  }) : assert(child != null),
       super(key: key);

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

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

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

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

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

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

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

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

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

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

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

178 179 180 181 182 183 184
  @override
  Widget build(BuildContext context) {
    return _AutofillScope(
      autofillScopeState: this,
      child: widget.child,
    );
  }
185 186 187 188 189 190 191 192 193 194 195 196

  @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:
197
        TextInput.finishAutofillContext();
198 199 200
        break;
    }
  }
201 202 203 204
}

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

211
  final AutofillGroupState? _scope;
212

213
  AutofillGroup get client => _scope!.widget;
214 215 216 217

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