// 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/scheduler.dart'; import 'basic.dart'; import 'container.dart'; import 'editable_text.dart'; import 'focus_manager.dart'; import 'framework.dart'; import 'overlay.dart'; /// The type of the [RawAutocomplete] callback which computes the list of /// optional completions for the widget's field based on the text the user has /// entered so far. /// /// See also: /// /// * [RawAutocomplete.optionsBuilder], which is of this type. typedef AutocompleteOptionsBuilder<T extends Object> = Iterable<T> Function(TextEditingValue textEditingValue); /// The type of the callback used by the [RawAutocomplete] widget to indicate /// that the user has selected an option. /// /// See also: /// /// * [RawAutocomplete.onSelected], which is of this type. typedef AutocompleteOnSelected<T extends Object> = void Function(T option); /// The type of the [RawAutocomplete] callback which returns a [Widget] that /// displays the specified [options] and calls [onSelected] if the user /// selects an option. /// /// See also: /// /// * [RawAutocomplete.optionsViewBuilder], which is of this type. 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: /// /// * [RawAutocomplete.fieldViewBuilder], which is of this type. typedef AutocompleteFieldViewBuilder = Widget Function( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted, ); /// The type of the [RawAutocomplete] callback that converts an option value to /// a string which can be displayed in the widget's options menu. /// /// See also: /// /// * [RawAutocomplete.displayStringForOption], which is of this type. typedef AutocompleteOptionToString<T extends Object> = String Function(T option); // TODO(justinmc): Mention AutocompleteCupertino when it is implemented. /// {@template flutter.widgets.RawAutocomplete.RawAutocomplete} /// 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]. /// {@endtemplate} /// /// This is a core framework widget with very basic UI. /// /// {@tool dartpad --template=freeform} /// This example shows how to create a very basic autocomplete widget using the /// [fieldViewBuilder] and [optionsViewBuilder] parameters. /// /// ```dart main /// import 'package:flutter/material.dart'; /// import 'package:flutter/widgets.dart'; /// /// void main() => runApp(const AutocompleteExampleApp()); /// /// class AutocompleteExampleApp extends StatelessWidget { /// const AutocompleteExampleApp({Key? key}) : super(key: key); /// /// @override /// Widget build(BuildContext context) { /// return MaterialApp( /// home: Scaffold( /// appBar: AppBar( /// title: const Text('RawAutocomplete Basic'), /// ), /// body: const Center( /// child: AutocompleteBasicExample(), /// ), /// ), /// ); /// } /// } /// /// class AutocompleteBasicExample extends StatelessWidget { /// const AutocompleteBasicExample({Key? key}) : super(key: key); /// /// static const List<String> _options = <String>[ /// 'aardvark', /// 'bobcat', /// 'chameleon', /// ]; /// /// @override /// Widget build(BuildContext context) { /// return RawAutocomplete<String>( /// optionsBuilder: (TextEditingValue textEditingValue) { /// return _options.where((String option) { /// return option.contains(textEditingValue.text.toLowerCase()); /// }); /// }, /// fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) { /// return TextFormField( /// controller: textEditingController, /// focusNode: focusNode, /// onFieldSubmitted: (String value) { /// onFieldSubmitted(); /// }, /// ); /// }, /// optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { /// return Align( /// alignment: Alignment.topLeft, /// child: Material( /// elevation: 4.0, /// child: SizedBox( /// height: 200.0, /// child: ListView.builder( /// padding: const EdgeInsets.all(8.0), /// itemCount: options.length, /// itemBuilder: (BuildContext context, int index) { /// final String option = options.elementAt(index); /// return GestureDetector( /// onTap: () { /// onSelected(option); /// }, /// child: ListTile( /// title: Text(option), /// ), /// ); /// }, /// ), /// ), /// ), /// ); /// }, /// ); /// } /// } /// ``` /// {@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. /// /// {@tool dartpad --template=freeform} /// This example is similar to the previous example, but it uses a custom T data /// type instead of directly using String. /// /// ```dart main /// import 'package:flutter/material.dart'; /// import 'package:flutter/widgets.dart'; /// /// void main() => runApp(const AutocompleteExampleApp()); /// /// class AutocompleteExampleApp extends StatelessWidget { /// const AutocompleteExampleApp({Key? key}) : super(key: key); /// /// @override /// Widget build(BuildContext context) { /// return MaterialApp( /// home: Scaffold( /// appBar: AppBar( /// title: const Text('RawAutocomplete Custom Type'), /// ), /// body: const Center( /// child: AutocompleteCustomTypeExample(), /// ), /// ), /// ); /// } /// } /// /// // An example of a type that someone might want to autocomplete a list of. /// @immutable /// class User { /// const User({ /// required this.email, /// required this.name, /// }); /// /// final String email; /// final String name; /// /// @override /// String toString() { /// return '$name, $email'; /// } /// /// @override /// bool operator ==(Object other) { /// if (other.runtimeType != runtimeType) { /// return false; /// } /// return other is User /// && other.name == name /// && other.email == email; /// } /// /// @override /// int get hashCode => hashValues(email, name); /// } /// /// class AutocompleteCustomTypeExample extends StatelessWidget { /// const AutocompleteCustomTypeExample({Key? key}) : super(key: key); /// /// static const List<User> _userOptions = <User>[ /// User(name: 'Alice', email: 'alice@example.com'), /// User(name: 'Bob', email: 'bob@example.com'), /// User(name: 'Charlie', email: 'charlie123@gmail.com'), /// ]; /// /// static String _displayStringForOption(User option) => option.name; /// /// @override /// Widget build(BuildContext context) { /// return RawAutocomplete<User>( /// optionsBuilder: (TextEditingValue textEditingValue) { /// return _userOptions.where((User option) { /// // Search based on User.toString, which includes both name and /// // email, even though the display string is just the name. /// return option.toString().contains(textEditingValue.text.toLowerCase()); /// }); /// }, /// displayStringForOption: _displayStringForOption, /// fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) { /// return TextFormField( /// controller: textEditingController, /// focusNode: focusNode, /// onFieldSubmitted: (String value) { /// onFieldSubmitted(); /// }, /// ); /// }, /// optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<User> onSelected, Iterable<User> options) { /// return Align( /// alignment: Alignment.topLeft, /// child: Material( /// elevation: 4.0, /// child: SizedBox( /// height: 200.0, /// child: ListView.builder( /// padding: const EdgeInsets.all(8.0), /// itemCount: options.length, /// itemBuilder: (BuildContext context, int index) { /// final User option = options.elementAt(index); /// return GestureDetector( /// onTap: () { /// onSelected(option); /// }, /// child: ListTile( /// title: Text(_displayStringForOption(option)), /// ), /// ); /// }, /// ), /// ), /// ), /// ); /// }, /// ); /// } /// } /// ``` /// {@end-tool} /// /// {@tool dartpad --template=freeform} /// This example shows the use of RawAutocomplete in a form. /// /// ```dart main /// import 'package:flutter/material.dart'; /// import 'package:flutter/widgets.dart'; /// /// void main() => runApp(const AutocompleteExampleApp()); /// /// class AutocompleteExampleApp extends StatelessWidget { /// const AutocompleteExampleApp({Key? key}) : super(key: key); /// /// @override /// Widget build(BuildContext context) { /// return MaterialApp( /// home: Scaffold( /// appBar: AppBar( /// title: const Text('RawAutocomplete Form'), /// ), /// body: const Center( /// child: AutocompleteFormExample(), /// ), /// ), /// ); /// } /// } /// /// class AutocompleteFormExample extends StatefulWidget { /// const AutocompleteFormExample({Key? key}) : super(key: key); /// /// @override /// AutocompleteFormExampleState createState() => AutocompleteFormExampleState(); /// } /// /// class AutocompleteFormExampleState extends State<AutocompleteFormExample> { /// final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); /// final TextEditingController _textEditingController = TextEditingController(); /// String? _dropdownValue; /// String? _autocompleteSelection; /// /// static const List<String> _options = <String>[ /// 'aardvark', /// 'bobcat', /// 'chameleon', /// ]; /// /// @override /// Widget build(BuildContext context) { /// return Form( /// key: _formKey, /// child: Column( /// children: <Widget>[ /// DropdownButtonFormField<String>( /// value: _dropdownValue, /// icon: const Icon(Icons.arrow_downward), /// hint: const Text('This is a regular DropdownButtonFormField'), /// iconSize: 24, /// elevation: 16, /// style: const TextStyle(color: Colors.deepPurple), /// onChanged: (String? newValue) { /// setState(() { /// _dropdownValue = newValue; /// }); /// }, /// items: <String>['One', 'Two', 'Free', 'Four'] /// .map<DropdownMenuItem<String>>((String value) { /// return DropdownMenuItem<String>( /// value: value, /// child: Text(value), /// ); /// }).toList(), /// validator: (String? value) { /// if (value == null) { /// return 'Must make a selection.'; /// } /// return null; /// }, /// ), /// TextFormField( /// controller: _textEditingController, /// decoration: const InputDecoration( /// hintText: 'This is a regular TextFormField', /// ), /// validator: (String? value) { /// if (value == null || value.isEmpty) { /// return 'Can\'t be empty.'; /// } /// return null; /// }, /// ), /// RawAutocomplete<String>( /// optionsBuilder: (TextEditingValue textEditingValue) { /// return _options.where((String option) { /// return option.contains(textEditingValue.text.toLowerCase()); /// }); /// }, /// onSelected: (String selection) { /// setState(() { /// _autocompleteSelection = selection; /// }); /// }, /// fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) { /// return TextFormField( /// controller: textEditingController, /// decoration: const InputDecoration( /// hintText: 'This is a RawAutocomplete!', /// ), /// focusNode: focusNode, /// onFieldSubmitted: (String value) { /// onFieldSubmitted(); /// }, /// validator: (String? value) { /// if (!_options.contains(value)) { /// return 'Nothing selected.'; /// } /// return null; /// }, /// ); /// }, /// optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { /// return Align( /// alignment: Alignment.topLeft, /// child: Material( /// elevation: 4.0, /// child: SizedBox( /// height: 200.0, /// child: ListView.builder( /// padding: const EdgeInsets.all(8.0), /// itemCount: options.length, /// itemBuilder: (BuildContext context, int index) { /// final String option = options.elementAt(index); /// return GestureDetector( /// onTap: () { /// onSelected(option); /// }, /// child: ListTile( /// title: Text(option), /// ), /// ); /// }, /// ), /// ), /// ), /// ); /// }, /// ), /// ElevatedButton( /// onPressed: () { /// FocusScope.of(context).requestFocus(new FocusNode()); /// if (!_formKey.currentState!.validate()) { /// return; /// } /// showDialog<void>( /// context: context, /// builder: (BuildContext context) { /// return AlertDialog( /// title: const Text('Successfully submitted'), /// content: SingleChildScrollView( /// child: ListBody( /// children: <Widget>[ /// Text('DropdownButtonFormField: "$_dropdownValue"'), /// Text('TextFormField: "${_textEditingController.text}"'), /// Text('RawAutocomplete: "$_autocompleteSelection"'), /// ], /// ), /// ), /// actions: <Widget>[ /// TextButton( /// child: const Text('Ok'), /// onPressed: () { /// Navigator.of(context).pop(); /// }, /// ), /// ], /// ); /// }, /// ); /// }, /// child: const Text('Submit'), /// ), /// ], /// ), /// ); /// } /// } /// ``` /// {@end-tool} /// /// See also: /// /// * [Autocomplete], which is a Material-styled implementation that is based /// on RawAutocomplete. class RawAutocomplete<T extends Object> extends StatefulWidget { /// Create an instance of RawAutocomplete. /// /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must /// not be null. const RawAutocomplete({ Key? key, required this.optionsViewBuilder, required this.optionsBuilder, this.displayStringForOption = defaultStringForOption, this.fieldViewBuilder, this.focusNode, this.onSelected, this.textEditingController, this.initialValue, }) : assert(displayStringForOption != null), 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.', ), assert(optionsBuilder != null), assert(optionsViewBuilder != null), assert((focusNode == null) == (textEditingController == null)), assert( !(textEditingController != null && initialValue != null), 'textEditingController and initialValue cannot be simultaneously defined.', ), super(key: key); /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder} /// Builds the field whose input is used to get the options. /// /// Pass the provided [TextEditingController] to the field built here so that /// RawAutocomplete can listen for changes. /// {@endtemplate} 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. /// /// {@tool dartpad --template=freeform} /// 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. /// /// ```dart main /// import 'package:flutter/material.dart'; /// import 'package:flutter/widgets.dart'; /// /// void main() => runApp(const AutocompleteExampleApp()); /// /// class AutocompleteExampleApp extends StatelessWidget { /// const AutocompleteExampleApp({Key? key}) : super(key: key); /// /// @override /// Widget build(BuildContext context) { /// return const MaterialApp( /// home: RawAutocompleteSplit(), /// ); /// } /// } /// /// const List<String> _options = <String>[ /// 'aardvark', /// 'bobcat', /// 'chameleon', /// ]; /// /// class RawAutocompleteSplit extends StatefulWidget { /// const RawAutocompleteSplit({Key? key}) : super(key: key); /// /// @override /// RawAutocompleteSplitState createState() => RawAutocompleteSplitState(); /// } /// /// class RawAutocompleteSplitState extends State<RawAutocompleteSplit> { /// final TextEditingController _textEditingController = TextEditingController(); /// final FocusNode _focusNode = FocusNode(); /// final GlobalKey _autocompleteKey = GlobalKey(); /// /// @override /// Widget build(BuildContext context) { /// return Scaffold( /// appBar: AppBar( /// // This is where the real field is being built. /// title: TextFormField( /// controller: _textEditingController, /// focusNode: _focusNode, /// decoration: const InputDecoration( /// hintText: 'Split RawAutocomplete App', /// ), /// onFieldSubmitted: (String value) { /// RawAutocomplete.onFieldSubmitted<String>(_autocompleteKey); /// }, /// ), /// ), /// body: Align( /// alignment: Alignment.topLeft, /// child: RawAutocomplete<String>( /// key: _autocompleteKey, /// focusNode: _focusNode, /// textEditingController: _textEditingController, /// optionsBuilder: (TextEditingValue textEditingValue) { /// return _options.where((String option) { /// return option.contains(textEditingValue.text.toLowerCase()); /// }).toList(); /// }, /// optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { /// return Material( /// elevation: 4.0, /// child: ListView( /// children: options.map((String option) => GestureDetector( /// onTap: () { /// onSelected(option); /// }, /// child: ListTile( /// title: Text(option), /// ), /// )).toList(), /// ), /// ); /// }, /// ), /// ), /// ); /// } /// } /// ``` /// {@end-tool} /// {@endtemplate} /// /// If this parameter is not null, then [textEditingController] must also be /// not null. final FocusNode? focusNode; /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder} /// 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 /// place in the widget tree as [RawAutocomplete]. /// {@endtemplate} final AutocompleteOptionsViewBuilder<T> optionsViewBuilder; /// {@template flutter.widgets.RawAutocomplete.displayStringForOption} /// 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()`. /// {@endtemplate} final AutocompleteOptionToString<T> displayStringForOption; /// {@template flutter.widgets.RawAutocomplete.onSelected} /// 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. /// {@endtemplate} final AutocompleteOnSelected<T>? onSelected; /// {@template flutter.widgets.RawAutocomplete.optionsBuilder} /// A function that returns the current selectable options objects given the /// current TextEditingValue. /// {@endtemplate} final AutocompleteOptionsBuilder<T> optionsBuilder; /// 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; /// {@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; /// 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(); } /// 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) { return option.toString(); } @override State<RawAutocomplete<T>> createState() => _RawAutocompleteState<T>(); } class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> { final GlobalKey _fieldKey = GlobalKey(); final LayerLink _optionsLayerLink = LayerLink(); late TextEditingController _textEditingController; late FocusNode _focusNode; Iterable<T> _options = Iterable<T>.empty(); T? _selection; // The OverlayEntry containing the options. OverlayEntry? _floatingOptions; // True iff the state indicates that the options should be visible. bool get _shouldShowOptions { return _focusNode.hasFocus && _selection == null && _options.isNotEmpty; } // Called when _textEditingController changes. void _onChangedField() { final Iterable<T> options = widget.optionsBuilder( _textEditingController.value, ); _options = options; if (_selection != null && _textEditingController.text != widget.displayStringForOption(_selection!)) { _selection = null; } _updateOverlay(); } // Called when the field's FocusNode changes. void _onChangedFocus() { _updateOverlay(); } // Called from fieldViewBuilder when the user submits the field. void _onFieldSubmitted() { if (_options.isEmpty) { return; } _select(_options.first); } // 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, ); widget.onSelected?.call(_selection!); } // Hide or show the options overlay, if needed. void _updateOverlay() { if (_shouldShowOptions) { _floatingOptions?.remove(); _floatingOptions = OverlayEntry( builder: (BuildContext context) { return CompositedTransformFollower( link: _optionsLayerLink, showWhenUnlinked: false, targetAnchor: Alignment.bottomLeft, child: widget.optionsViewBuilder(context, _select, _options), ); }, ); Overlay.of(context, rootOverlay: true)!.insert(_floatingOptions!); } else if (_floatingOptions != null) { _floatingOptions!.remove(); _floatingOptions = null; } } // 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); } @override void initState() { super.initState(); _textEditingController = widget.textEditingController ?? TextEditingController.fromValue(widget.initialValue); _textEditingController.addListener(_onChangedField); _focusNode = widget.focusNode ?? FocusNode(); _focusNode.addListener(_onChangedFocus); SchedulerBinding.instance!.addPostFrameCallback((Duration _) { _updateOverlay(); }); } @override void didUpdateWidget(RawAutocomplete<T> oldWidget) { super.didUpdateWidget(oldWidget); _updateTextEditingController( oldWidget.textEditingController, widget.textEditingController, ); _updateFocusNode(oldWidget.focusNode, widget.focusNode); SchedulerBinding.instance!.addPostFrameCallback((Duration _) { _updateOverlay(); }); } @override void dispose() { _textEditingController.removeListener(_onChangedField); if (widget.textEditingController == null) { _textEditingController.dispose(); } _focusNode.removeListener(_onChangedFocus); if (widget.focusNode == null) { _focusNode.dispose(); } _floatingOptions?.remove(); _floatingOptions = null; super.dispose(); } @override Widget build(BuildContext context) { return Container( key: _fieldKey, child: CompositedTransformTarget( link: _optionsLayerLink, child: widget.fieldViewBuilder == null ? const SizedBox.shrink() : widget.fieldViewBuilder!( context, _textEditingController, _focusNode, _onFieldSubmitted, ), ), ); } }