autocomplete.dart 22.5 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'dart:async';

7
import 'package:flutter/scheduler.dart';
8
import 'package:flutter/services.dart';
9

10
import 'actions.dart';
11 12 13 14 15
import 'basic.dart';
import 'container.dart';
import 'editable_text.dart';
import 'focus_manager.dart';
import 'framework.dart';
16
import 'inherited_notifier.dart';
17
import 'overlay.dart';
18
import 'shortcuts.dart';
19
import 'tap_region.dart';
20

21 22 23
// Examples can assume:
// late BuildContext context;

24
/// The type of the [RawAutocomplete] callback which computes the list of
25
/// optional completions for the widget's field, based on the text the user has
26 27 28
/// entered so far.
///
/// See also:
29
///
30
///   * [RawAutocomplete.optionsBuilder], which is of this type.
31
typedef AutocompleteOptionsBuilder<T extends Object> = FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue);
32

33
/// The type of the callback used by the [RawAutocomplete] widget to indicate
34 35 36
/// that the user has selected an option.
///
/// See also:
37
///
38
///   * [RawAutocomplete.onSelected], which is of this type.
39 40
typedef AutocompleteOnSelected<T extends Object> = void Function(T option);

41
/// The type of the [RawAutocomplete] callback which returns a [Widget] that
42 43 44
/// displays the specified [options] and calls [onSelected] if the user
/// selects an option.
///
45 46 47 48 49
/// The returned widget from this callback will be wrapped in an
/// [AutocompleteHighlightedOption] inherited widget. This will allow
/// this callback to determine which option is currently highlighted for
/// keyboard navigation.
///
50
/// See also:
51
///
52
///   * [RawAutocomplete.optionsViewBuilder], which is of this type.
53 54 55 56 57 58 59 60 61 62
typedef AutocompleteOptionsViewBuilder<T extends Object> = Widget Function(
  BuildContext context,
  AutocompleteOnSelected<T> onSelected,
  Iterable<T> options,
);

/// The type of the Autocomplete callback which returns the widget that
/// contains the input [TextField] or [TextFormField].
///
/// See also:
63
///
64
///   * [RawAutocomplete.fieldViewBuilder], which is of this type.
65 66 67 68 69 70 71
typedef AutocompleteFieldViewBuilder = Widget Function(
  BuildContext context,
  TextEditingController textEditingController,
  FocusNode focusNode,
  VoidCallback onFieldSubmitted,
);

72
/// The type of the [RawAutocomplete] callback that converts an option value to
73 74 75
/// a string which can be displayed in the widget's options menu.
///
/// See also:
76
///
77
///   * [RawAutocomplete.displayStringForOption], which is of this type.
78 79
typedef AutocompleteOptionToString<T extends Object> = String Function(T option);

80 81
// TODO(justinmc): Mention AutocompleteCupertino when it is implemented.
/// {@template flutter.widgets.RawAutocomplete.RawAutocomplete}
82 83 84 85 86 87
/// A widget for helping the user make a selection by entering some text and
/// choosing from among a list of options.
///
/// The user's text input is received in a field built with the
/// [fieldViewBuilder] parameter. The options to be displayed are determined
/// using [optionsBuilder] and rendered with [optionsViewBuilder].
88 89 90
/// {@endtemplate}
///
/// This is a core framework widget with very basic UI.
91
///
92
/// {@tool dartpad}
93 94 95
/// This example shows how to create a very basic autocomplete widget using the
/// [fieldViewBuilder] and [optionsViewBuilder] parameters.
///
96
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart **
97 98 99 100 101 102 103 104
/// {@end-tool}
///
/// The type parameter T represents the type of the options. Most commonly this
/// is a String, as in the example above. However, it's also possible to use
/// another type with a `toString` method, or a custom [displayStringForOption].
/// Options will be compared using `==`, so it may be beneficial to override
/// [Object.==] and [Object.hashCode] for custom types.
///
105
/// {@tool dartpad}
106 107 108
/// This example is similar to the previous example, but it uses a custom T data
/// type instead of directly using String.
///
109
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart **
110 111
/// {@end-tool}
///
112
/// {@tool dartpad}
113
/// This example shows the use of RawAutocomplete in a form.
114
///
115
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart **
116
/// {@end-tool}
117 118 119 120 121
///
/// See also:
///
///  * [Autocomplete], which is a Material-styled implementation that is based
/// on RawAutocomplete.
122 123
class RawAutocomplete<T extends Object> extends StatefulWidget {
  /// Create an instance of RawAutocomplete.
124
  ///
125 126
  /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
  /// not be null.
127
  const RawAutocomplete({
128
    super.key,
129 130
    required this.optionsViewBuilder,
    required this.optionsBuilder,
131
    this.displayStringForOption = defaultStringForOption,
132 133
    this.fieldViewBuilder,
    this.focusNode,
134
    this.onSelected,
135
    this.textEditingController,
136
    this.initialValue,
137
  }) : assert(displayStringForOption != null),
138 139 140 141 142
       assert(
         fieldViewBuilder != null
            || (key != null && focusNode != null && textEditingController != null),
         'Pass in a fieldViewBuilder, or otherwise create a separate field and pass in the FocusNode, TextEditingController, and a key. Use the key with RawAutocomplete.onFieldSubmitted.',
        ),
143 144
       assert(optionsBuilder != null),
       assert(optionsViewBuilder != null),
145
       assert((focusNode == null) == (textEditingController == null)),
146 147
       assert(
         !(textEditingController != null && initialValue != null),
148
         'textEditingController and initialValue cannot be simultaneously defined.',
149
       );
150

151
  /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
152 153 154
  /// Builds the field whose input is used to get the options.
  ///
  /// Pass the provided [TextEditingController] to the field built here so that
155
  /// RawAutocomplete can listen for changes.
156
  /// {@endtemplate}
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  final AutocompleteFieldViewBuilder? fieldViewBuilder;

  /// The [FocusNode] that is used for the text field.
  ///
  /// {@template flutter.widgets.RawAutocomplete.split}
  /// The main purpose of this parameter is to allow the use of a separate text
  /// field located in another part of the widget tree instead of the text
  /// field built by [fieldViewBuilder]. For example, it may be desirable to
  /// place the text field in the AppBar and the options below in the main body.
  ///
  /// When following this pattern, [fieldViewBuilder] can return
  /// `SizedBox.shrink()` so that nothing is drawn where the text field would
  /// normally be. A separate text field can be created elsewhere, and a
  /// FocusNode and TextEditingController can be passed both to that text field
  /// and to RawAutocomplete.
  ///
173
  /// {@tool dartpad}
174 175 176
  /// This examples shows how to create an autocomplete widget with the text
  /// field in the AppBar and the results in the main body of the app.
  ///
177
  /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
178 179 180 181 182 183
  /// {@end-tool}
  /// {@endtemplate}
  ///
  /// If this parameter is not null, then [textEditingController] must also be
  /// not null.
  final FocusNode? focusNode;
184

185
  /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
186 187 188 189
  /// Builds the selectable options widgets from a list of options objects.
  ///
  /// The options are displayed floating below the field using a
  /// [CompositedTransformFollower] inside of an [Overlay], not at the same
190
  /// place in the widget tree as [RawAutocomplete].
191 192 193 194 195 196 197 198 199
  ///
  /// In order to track which item is highlighted by keyboard navigation, the
  /// resulting options will be wrapped in an inherited
  /// [AutocompleteHighlightedOption] widget.
  /// Inside this callback, the index of the highlighted option can be obtained
  /// from [AutocompleteHighlightedOption.of] to display the highlighted option
  /// with a visual highlight to indicate it will be the option selected from
  /// the keyboard.
  ///
200
  /// {@endtemplate}
201 202
  final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;

203
  /// {@template flutter.widgets.RawAutocomplete.displayStringForOption}
204 205 206 207 208 209
  /// Returns the string to display in the field when the option is selected.
  ///
  /// This is useful when using a custom T type and the string to display is
  /// different than the string to search by.
  ///
  /// If not provided, will use `option.toString()`.
210
  /// {@endtemplate}
211 212
  final AutocompleteOptionToString<T> displayStringForOption;

213
  /// {@template flutter.widgets.RawAutocomplete.onSelected}
214 215 216 217 218
  /// Called when an option is selected by the user.
  ///
  /// Any [TextEditingController] listeners will not be called when the user
  /// selects an option, even though the field will update with the selected
  /// value, so use this to be informed of selection.
219
  /// {@endtemplate}
220 221
  final AutocompleteOnSelected<T>? onSelected;

222
  /// {@template flutter.widgets.RawAutocomplete.optionsBuilder}
223 224
  /// A function that returns the current selectable options objects given the
  /// current TextEditingValue.
225
  /// {@endtemplate}
226 227
  final AutocompleteOptionsBuilder<T> optionsBuilder;

228 229 230 231 232 233 234
  /// The [TextEditingController] that is used for the text field.
  ///
  /// {@macro flutter.widgets.RawAutocomplete.split}
  ///
  /// If this parameter is not null, then [focusNode] must also be not null.
  final TextEditingController? textEditingController;

235 236 237 238 239 240 241 242 243 244
  /// {@template flutter.widgets.RawAutocomplete.initialValue}
  /// The initial value to use for the text field.
  /// {@endtemplate}
  ///
  /// Setting the initial value does not notify [textEditingController]'s
  /// listeners, and thus will not cause the options UI to appear.
  ///
  /// This parameter is ignored if [textEditingController] is defined.
  final TextEditingValue? initialValue;

245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
  /// Calls [AutocompleteFieldViewBuilder]'s onFieldSubmitted callback for the
  /// RawAutocomplete widget indicated by the given [GlobalKey].
  ///
  /// This is not typically used unless a custom field is implemented instead of
  /// using [fieldViewBuilder]. In the typical case, the onFieldSubmitted
  /// callback is passed via the [AutocompleteFieldViewBuilder] signature. When
  /// not using fieldViewBuilder, the same callback can be called by using this
  /// static method.
  ///
  /// See also:
  ///
  ///  * [focusNode] and [textEditingController], which contain a code example
  ///    showing how to create a separate field outside of fieldViewBuilder.
  static void onFieldSubmitted<T extends Object>(GlobalKey key) {
    final _RawAutocompleteState<T> rawAutocomplete = key.currentState! as _RawAutocompleteState<T>;
    rawAutocomplete._onFieldSubmitted();
  }

263 264 265 266 267
  /// The default way to convert an option to a string in
  /// [displayStringForOption].
  ///
  /// Simply uses the `toString` method on the option.
  static String defaultStringForOption(dynamic option) {
268 269 270 271
    return option.toString();
  }

  @override
272
  State<RawAutocomplete<T>> createState() => _RawAutocompleteState<T>();
273 274
}

275
class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> {
276 277
  final GlobalKey _fieldKey = GlobalKey();
  final LayerLink _optionsLayerLink = LayerLink();
278 279
  late TextEditingController _textEditingController;
  late FocusNode _focusNode;
280 281 282
  late final Map<Type, Action<Intent>> _actionMap;
  late final _AutocompleteCallbackAction<AutocompletePreviousOptionIntent> _previousOptionAction;
  late final _AutocompleteCallbackAction<AutocompleteNextOptionIntent> _nextOptionAction;
283
  late final _AutocompleteCallbackAction<DismissIntent> _hideOptionsAction;
284 285
  Iterable<T> _options = Iterable<T>.empty();
  T? _selection;
286 287
  bool _userHidOptions = false;
  String _lastFieldText = '';
288 289 290 291 292 293
  final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0);

  static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{
    SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(),
    SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(),
  };
294 295 296 297 298 299

  // The OverlayEntry containing the options.
  OverlayEntry? _floatingOptions;

  // True iff the state indicates that the options should be visible.
  bool get _shouldShowOptions {
300
    return !_userHidOptions && _focusNode.hasFocus && _selection == null && _options.isNotEmpty;
301 302 303
  }

  // Called when _textEditingController changes.
304
  Future<void> _onChangedField() async {
305
    final TextEditingValue value = _textEditingController.value;
306
    final Iterable<T> options = await widget.optionsBuilder(
307
      value,
308 309
    );
    _options = options;
310
    _updateHighlight(_highlightedOptionIndex.value);
311
    if (_selection != null
312
        && value.text != widget.displayStringForOption(_selection!)) {
313 314
      _selection = null;
    }
315 316 317 318 319 320 321

    // Make sure the options are no longer hidden if the content of the field
    // changes (ignore selection changes).
    if (value.text != _lastFieldText) {
      _userHidOptions = false;
      _lastFieldText = value.text;
    }
322
    _updateActions();
323 324 325 326 327
    _updateOverlay();
  }

  // Called when the field's FocusNode changes.
  void _onChangedFocus() {
328 329
    // Options should no longer be hidden when the field is re-focused.
    _userHidOptions = !_focusNode.hasFocus;
330
    _updateActions();
331 332 333 334 335
    _updateOverlay();
  }

  // Called from fieldViewBuilder when the user submits the field.
  void _onFieldSubmitted() {
336
    if (_options.isEmpty || _userHidOptions) {
337 338
      return;
    }
339
    _select(_options.elementAt(_highlightedOptionIndex.value));
340 341 342 343 344 345 346 347 348 349 350 351 352
  }

  // Select the given option and update the widget.
  void _select(T nextSelection) {
    if (nextSelection == _selection) {
      return;
    }
    _selection = nextSelection;
    final String selectionString = widget.displayStringForOption(nextSelection);
    _textEditingController.value = TextEditingValue(
      selection: TextSelection.collapsed(offset: selectionString.length),
      text: selectionString,
    );
353 354
    _updateActions();
    _updateOverlay();
355 356 357
    widget.onSelected?.call(_selection!);
  }

358 359 360 361 362
  void _updateHighlight(int newIndex) {
    _highlightedOptionIndex.value = _options.isEmpty ? 0 : newIndex % _options.length;
  }

  void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
363 364
    if (_userHidOptions) {
      _userHidOptions = false;
365
      _updateActions();
366 367 368
      _updateOverlay();
      return;
    }
369 370 371 372
    _updateHighlight(_highlightedOptionIndex.value - 1);
  }

  void _highlightNextOption(AutocompleteNextOptionIntent intent) {
373 374
    if (_userHidOptions) {
      _userHidOptions = false;
375
      _updateActions();
376 377 378
      _updateOverlay();
      return;
    }
379 380 381
    _updateHighlight(_highlightedOptionIndex.value + 1);
  }

382
  Object? _hideOptions(DismissIntent intent) {
383 384
    if (!_userHidOptions) {
      _userHidOptions = true;
385
      _updateActions();
386
      _updateOverlay();
387
      return null;
388
    }
389
    return Actions.invoke(context, intent);
390 391
  }

392 393 394 395 396 397 398
  void _setActionsEnabled(bool enabled) {
    // The enabled state determines whether the action will consume the
    // key shortcut or let it continue on to the underlying text field.
    // They should only be enabled when the options are showing so shortcuts
    // can be used to navigate them.
    _previousOptionAction.enabled = enabled;
    _nextOptionAction.enabled = enabled;
399
    _hideOptionsAction.enabled = enabled;
400 401
  }

402 403 404 405 406
  void _updateActions() {
    _setActionsEnabled(_focusNode.hasFocus && _selection == null && _options.isNotEmpty);
  }

  bool _floatingOptionsUpdateScheduled = false;
407 408
  // Hide or show the options overlay, if needed.
  void _updateOverlay() {
409 410 411 412 413 414 415 416 417 418 419 420
    if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      if (!_floatingOptionsUpdateScheduled) {
        _floatingOptionsUpdateScheduled = true;
        SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
          _floatingOptionsUpdateScheduled = false;
          _updateOverlay();
        });
      }
      return;
    }

    _floatingOptions?.remove();
421
    if (_shouldShowOptions) {
422
      final OverlayEntry newFloatingOptions = OverlayEntry(
423 424 425 426 427
        builder: (BuildContext context) {
          return CompositedTransformFollower(
            link: _optionsLayerLink,
            showWhenUnlinked: false,
            targetAnchor: Alignment.bottomLeft,
428 429 430 431 432 433 434 435 436
            child: TextFieldTapRegion(
              child: AutocompleteHighlightedOption(
                highlightIndexNotifier: _highlightedOptionIndex,
                child: Builder(
                  builder: (BuildContext context) {
                    return widget.optionsViewBuilder(context, _select, _options);
                  }
                )
              ),
437
            ),
438 439 440
          );
        },
      );
441
      Overlay.of(context, rootOverlay: true, debugRequiredFor: widget).insert(newFloatingOptions);
442 443
      _floatingOptions = newFloatingOptions;
    } else {
444 445 446 447
      _floatingOptions = null;
    }
  }

448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
  // Handle a potential change in textEditingController by properly disposing of
  // the old one and setting up the new one, if needed.
  void _updateTextEditingController(TextEditingController? old, TextEditingController? current) {
    if ((old == null && current == null) || old == current) {
      return;
    }
    if (old == null) {
      _textEditingController.removeListener(_onChangedField);
      _textEditingController.dispose();
      _textEditingController = current!;
    } else if (current == null) {
      _textEditingController.removeListener(_onChangedField);
      _textEditingController = TextEditingController();
    } else {
      _textEditingController.removeListener(_onChangedField);
      _textEditingController = current;
    }
    _textEditingController.addListener(_onChangedField);
  }

  // Handle a potential change in focusNode by properly disposing of the old one
  // and setting up the new one, if needed.
  void _updateFocusNode(FocusNode? old, FocusNode? current) {
    if ((old == null && current == null) || old == current) {
      return;
    }
    if (old == null) {
      _focusNode.removeListener(_onChangedFocus);
      _focusNode.dispose();
      _focusNode = current!;
    } else if (current == null) {
      _focusNode.removeListener(_onChangedFocus);
      _focusNode = FocusNode();
    } else {
      _focusNode.removeListener(_onChangedFocus);
      _focusNode = current;
    }
    _focusNode.addListener(_onChangedFocus);
  }

488 489 490
  @override
  void initState() {
    super.initState();
491
    _textEditingController = widget.textEditingController ?? TextEditingController.fromValue(widget.initialValue);
492
    _textEditingController.addListener(_onChangedField);
493
    _focusNode = widget.focusNode ?? FocusNode();
494
    _focusNode.addListener(_onChangedFocus);
495 496
    _previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption);
    _nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption);
497
    _hideOptionsAction = _AutocompleteCallbackAction<DismissIntent>(onInvoke: _hideOptions);
498 499 500
    _actionMap = <Type, Action<Intent>> {
      AutocompletePreviousOptionIntent: _previousOptionAction,
      AutocompleteNextOptionIntent: _nextOptionAction,
501
      DismissIntent: _hideOptionsAction,
502
    };
503 504
    _updateActions();
    _updateOverlay();
505 506 507
  }

  @override
508
  void didUpdateWidget(RawAutocomplete<T> oldWidget) {
509
    super.didUpdateWidget(oldWidget);
510 511 512 513 514
    _updateTextEditingController(
      oldWidget.textEditingController,
      widget.textEditingController,
    );
    _updateFocusNode(oldWidget.focusNode, widget.focusNode);
515 516
    _updateActions();
    _updateOverlay();
517 518 519 520 521
  }

  @override
  void dispose() {
    _textEditingController.removeListener(_onChangedField);
522 523 524
    if (widget.textEditingController == null) {
      _textEditingController.dispose();
    }
525
    _focusNode.removeListener(_onChangedFocus);
526 527 528
    if (widget.focusNode == null) {
      _focusNode.dispose();
    }
529 530 531 532 533 534 535
    _floatingOptions?.remove();
    _floatingOptions = null;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
    return TextFieldTapRegion(
      child: Container(
        key: _fieldKey,
        child: Shortcuts(
          shortcuts: _shortcuts,
          child: Actions(
            actions: _actionMap,
            child: CompositedTransformTarget(
              link: _optionsLayerLink,
              child: widget.fieldViewBuilder == null
                ? const SizedBox.shrink()
                : widget.fieldViewBuilder!(
                    context,
                    _textEditingController,
                    _focusNode,
                    _onFieldSubmitted,
                  ),
            ),
554 555
          ),
        ),
556 557 558 559
      ),
    );
  }
}
560 561 562

class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
  _AutocompleteCallbackAction({
563
    required super.onInvoke,
564
    this.enabled = true,
565
  });
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598

  bool enabled;

  @override
  bool isEnabled(covariant T intent) => enabled;

  @override
  bool consumesKey(covariant T intent) => enabled;
}

/// An [Intent] to highlight the previous option in the autocomplete list.
class AutocompletePreviousOptionIntent extends Intent {
  /// Creates an instance of AutocompletePreviousOptionIntent.
  const AutocompletePreviousOptionIntent();
}

/// An [Intent] to highlight the next option in the autocomplete list.
class AutocompleteNextOptionIntent extends Intent {
  /// Creates an instance of AutocompleteNextOptionIntent.
  const AutocompleteNextOptionIntent();
}

/// An inherited widget used to indicate which autocomplete option should be
/// highlighted for keyboard navigation.
///
/// The `RawAutoComplete` widget will wrap the options view generated by the
/// `optionsViewBuilder` with this widget to provide the highlighted option's
/// index to the builder.
///
/// In the builder callback the index of the highlighted option can be obtained
/// by using the static [of] method:
///
/// ```dart
599
/// int highlightedIndex = AutocompleteHighlightedOption.of(context);
600 601 602 603 604 605 606
/// ```
///
/// which can then be used to tell which option should be given a visual
/// indication that will be the option selected with the keyboard.
class AutocompleteHighlightedOption extends InheritedNotifier<ValueNotifier<int>> {
  /// Create an instance of AutocompleteHighlightedOption inherited widget.
  const AutocompleteHighlightedOption({
607
    super.key,
608
    required ValueNotifier<int> highlightIndexNotifier,
609 610
    required super.child,
  }) : super(notifier: highlightIndexNotifier);
611 612 613 614 615 616 617 618 619

  /// Returns the index of the highlighted option from the closest
  /// [AutocompleteHighlightedOption] ancestor.
  ///
  /// If there is no ancestor, it returns 0.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
620
  /// int highlightedIndex = AutocompleteHighlightedOption.of(context);
621 622 623 624 625
  /// ```
  static int of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AutocompleteHighlightedOption>()?.notifier?.value ?? 0;
  }
}