Detect TextFormField stop typing in flutter - flutter

I have TextFormField, and want do same actions when user stop typing in textfield. Now I am using onchange function, but I want detect when user stop typing.

If you want to achieve debounce on textfield for searching, then here you go.
final _searchQueryController = new TextEditingController();
Timer _debounce;
String query = "";
int _debouncetime = 500;
#override
void initState() {
_searchQueryController.addListener(_onSearchChanged);
super.initState();
}
#override
void dispose() {
_searchQueryController.removeListener(_onSearchChanged);
_searchQueryController.dispose();
super.dispose();
}
_onSearchChanged() {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(Duration(milliseconds: _debouncetime), () {
if (_searchQueryController.text != "") {
///here you perform your search
performSearch(_searchQueryController.text);
}
});
}
//your textfield
TextField(controller: _searchQueryController,
autofocus: true,
decoration: InputDecoration(
hintText: " Search...",
border: InputBorder.none,
),
style: TextStyle(fontSize: 14.0),
)

You can do it with flutter_hooks as follows:
class DebounceTextField extends HookWidget {
///
const DebounceTextField({
Key? key,
required this.padding,
required this.onAnswer,
required this.child,
this.initialText,
this.debounceTime,
}) : super(key: key);
///
final EdgeInsets padding;
///
final String? initialText;
///
final OnAnswer onAnswer;
///
final TextFormField child;
///
final int? debounceTime;
#override
Widget build(BuildContext context) {
final TextEditingController textController =
useTextEditingController(text: initialText);
useEffect(
() {
Timer? timer;
void listener() {
timer?.cancel();
timer = Timer(
Duration(milliseconds: debounceTime ?? 500),
() => onAnswer(textController.text),
);
}
textController.addListener(listener);
return () {
timer?.cancel();
textController.removeListener(listener);
};
},
<TextEditingController>[textController],
);
// child.controller = textController;
return Padding(
padding: padding,
child: TextFormField(
controller: textController,
validator: _shortAnswerValidator,
decoration: const InputDecoration(
hintText: "Cevabı buraya yazınız...",
),
),
);
}
}
We got the inspiration for this one here.

Related

Flutter - How to implement inline editing?

I found a similar behaviour on Safari:
The problem statement is that on tap of normal text, the text field should be editable and we can able to edit the text.
Have an example
In this scenario, we can see how on tap of initial text the editable text field is getting displayed and we can able to edit the text.
So let’s start with the process:
First, we have to initialize the variables.
bool _isEditingText = false;
TextEditingController _editingController;
String initialText = "Initial Text";
_isEditingText is the boolean variable and we have to set it false because we have to make it true when the user is tap on text.
TextEditingController -whenever a user modifies a text field with an associated TextEditingController, the text field edits and the controller notifies the listener. Listeners can then read the text and selection properties that the user has typed and updated. Basically the text editing controller is used to get the updated value from the text field.
initialText -Initial value, set to the text.
When we use any type of controller then we have to initialize and dispose of the controller.
So first initialize the controller in init state.
#override
void initState() {
super.initState();
_editingController = TextEditingController(text: initialText);
}
#override
void dispose() {
_editingController.dispose();
super.dispose();
}
‘dispose()’ is called when the State object is removed, which is permanent.
This method is used to unsubscribe and cancel all animations, streams, etc.
The framework calls this method when this state object will never build again.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("'Editable Text"),
),
body: Center(
child: _editTitleTextField(),
),
);
}
In build widget, I am simply displaying a widget _editTitleTextField().
Widget _editTitleTextField() {
if (_isEditingText)
return Center(
child: TextField(
onSubmitted: (newValue){
setState(() {
initialText = newValue;
_isEditingText =false;
});
},
autofocus: true,
controller: _editingController,
),
);
return InkWell(
onTap: () {
setState(() {
_isEditingText = true;
});
},
child: Text(
initialText,
style: TextStyle(
color: Colors.black,
fontSize: 18.0,
),
);
}
so what exactly the _editTitleTextField() widget does is, if the value of _isEditingText is false then simply show the text and on tap of text set the value of _isEditingText to true.
When _isEditingText is true then _editTitleTextField return text field. Textfield has parameter onSubmitted, so in onSubmitted method new value is assigned to initialText which is the updated value getting from _editingController.
Tada! This way we make the text editable and update the value of the text!
This can be done by having a StatefulWidget that keeps track of the state, by using a boolean for example. Let's say this bool is named editing. When editing == false you display a Text widget with the text to display, wrapped within a handler for the tap, such as a GestureDetector. When the user taps, the state is changed to editing = true. In this state you'll display a TextField with this value. You can use a TextEditingController within in this StatefulWidget and update its value there. This would allow you to up have all text selected when the user taps and changes to the editing state
I believe this is the correct answer:
class InlineEditableText extends StatefulWidget {
const InlineEditableText({
Key? key,
required this.text,
required this.style,
}) : super(key: key);
final String text;
final TextStyle style;
#override
State<InlineEditableText> createState() => _InlineEditableTextState();
}
class _InlineEditableTextState extends State<InlineEditableText> {
var _isEditing = false;
final _focusNode = FocusNode();
late String _text = widget.text;
late final TextStyle _style = widget.style;
late TextEditingController _controller;
#override
void initState() {
_controller = TextEditingController(text: _text);
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
setState(() => _isEditing = false);
} else {
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: _controller.value.text.runes.length,
);
}
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: () => setState(() {
_isEditing = !_isEditing;
_focusNode.requestFocus();
}),
child: TextField(
maxLines: 1,
style: _style,
focusNode: _focusNode,
controller: _controller,
onSubmitted: (changed) {
setState(() {
_text = changed;
_isEditing = false;
});
},
showCursor: _isEditing,
cursorColor: Colors.black,
enableInteractiveSelection: _isEditing,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 0,
vertical: 4.4,
),
border: _isEditing
? const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(0)),
)
: InputBorder.none,
),
),
);
}
}

Flutter - change TextFormField background when in active mode (Typing)

I want to achieve this.
While a text form field is inactive, its background, fill color will be grey. But when I am typing or it is in active mode, its background color will be white.
How to achieve this behavior?
try this:
class CustomTextFiled extends StatefulWidget {
const CustomTextFiled({
Key? key,
this.focusNode,
required this.fillColor,
required this.focusColor,
// add whaterver properties that your textfield needs. like controller and ..
}) : super(key: key);
final FocusNode? focusNode;
final Color focusColor;
final Color fillColor;
#override
_CustomTextFiledState createState() => _CustomTextFiledState();
}
class _CustomTextFiledState extends State<CustomTextFiled> {
late FocusNode focusNode;
#override
void initState() {
focusNode = widget.focusNode ?? FocusNode();
focusNode.addListener(() {
setState(() {});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return TextField(
focusNode: focusNode,
decoration: InputDecoration(
filled: true,
fillColor: focusNode.hasFocus ? widget.focusColor : widget.fillColor,
),
);
}
}
You can use FocusNode with listener.
late final FocusNode focusNode = FocusNode()
..addListener(() {
setState(() {});
});
....
TextField(
focusNode: focusNode,
decoration: InputDecoration(
fillColor: focusNode.hasFocus ? Colors.white : null,
filled: focusNode.hasFocus ? true : null,
),
)
After going through some tests, I have finalized the correct answer. The above answer is good. The first one has a problem. Focus Node variable must be inside the state class so that it can preserve its state.
class _GlobalTextFormFieldState extends State<GlobalTextFormField> {
late FocusNode focusNode;
#override
void initState() {
focusNode = FocusNode();
focusNode.addListener(() {
setState(() {});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return TextFormField(
focusNode: focusNode,
);
}
}

Flutter formKey currentstate is null when pass to another widget

I'm trying to create a Textbutton widget with a disabled property like this:
class AppTextButton extends StatelessWidget {
final String title;
final void Function(BuildContext context) onPress;
final EdgeInsetsGeometry margin;
final EdgeInsetsGeometry padding;
final double borderRadius;
final Color backgroundColor;
final Image? leadingIcon;
final Image? trailingIcon;
final TextStyle? textStyle;
final bool disabled;
AppTextButton(this.title, this.onPress,
{this.margin = const EdgeInsets.all(0),
this.padding = const EdgeInsets.all(12),
this.borderRadius = 0,
this.leadingIcon,
this.trailingIcon,
this.textStyle,
this.disabled = false,
this.backgroundColor = const Color(0xFFFFFFFF)});
#override
Widget build(BuildContext context) {
return Container(
padding: margin,
child: TextButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius))),
backgroundColor: MaterialStateProperty.all(backgroundColor)),
child: Row(
children: [
if (this.leadingIcon != null) ...[this.leadingIcon!],
Expanded(
child: Padding(
padding: padding,
child:
Text(title, textAlign: TextAlign.center, style: textStyle),
),
),
if (this.trailingIcon != null) ...[this.trailingIcon!]
],
),
onPressed: () => !disabled ? onPress(context) : null,
),
);
}
}
And in my screen, I declare my formKey and my form as following:
class LoginScreen extends AppBaseScreen {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
Form(
key: _formKey,
child: Obx(
() => AppTextInput(
"Please input passcode",
_passwordController,
borderRadius: 8,
fillColor: Color(0xFFF6F4F5),
keyboardType: TextInputType.number,
errorMessage: _c.errorLoginConfirm.value,
isObscure: true,
onChange: _onInputChange,
maxLength: 6,
margin: EdgeInsets.only(top: 12, left: 20, right: 20),
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
),
)),
And I will have a button at the bottom of the screen, which I pass the !_formKey.currentState!.validate() in the disabled field
AppTextButton("Login", _onLogin,
margin: EdgeInsets.fromLTRB(24, 24, 24, 8),
backgroundColor: Color(0xFFFF353C),
disabled: !_formKey.currentState!.validate(),
textStyle: TextStyle(color: Colors.white),
borderRadius: 8),
However, the formKey.currentState is null and throw the following error everytime the screen is opened.
Null check operator used on a null value
What I am doing wrong here? Thank you in advance!
You need to save the form state before passing,
final FormState formState = _formKey.currentState;
formState.save();
onPressed: () {
FocusScope.of(context).requestFocus(FocusNode());
final FormState formState = _formKey.currentState;
if (formState.validate()) {
formState.save();
onPress(context);
}
},
I think the problem is caused because all the widgets are created at the same time, so the _formKey.currentState is still null when the AppTextButton calls it.
You need to create a separate controller to control the state of the button and add it to the validator like this:
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
buttonDisableController = true;
return "Passcode must have at least 6 digits";
}
}
buttonDisableController = false;
return null;
},
In your case, you should know how the widgets building process (Assume you have Botton widget and Input widget):
Botton and Input are building initial state. both states are not yet ready to be read and used
Botton and Input are built. States are ready to read.
User interact to Input. Input must call Button to rebuild its state if the value passes the validator
Botton rebuild.
For the process, you should change your code like:
Get and modify the state of Button inside Input
Notify Button to rebuild
There are many ways to handle the state management between widgets. I simply change the AppTextButton into Statefultwidget to achieve it.
...
final _buttonKey = GlobalKey<_AppTextButtonState>();
...
AppTextButton(key: _buttonKey)
...
class AppTextButton extends StatefulWidget {
final bool initDisable;
AppTextButton({
this.initDisable = false,
Key? key,
}) : super(key: key);
#override
_AppTextButtonState createState() => _AppTextButtonState();
}
class _AppTextButtonState extends State<AppTextButton> {
var disable;
#override
void initState() {
disable = widget.initDisable;
super.initState();
}
#override
Widget build(BuildContext context) {
return TextButton(child: Text('Button'), onPressed: disable ? null : () {});
}
void enableButton() {
setState(() {
disable = false;
});
}
void disableButton() {
setState(() {
disable = true;
});
}
}
class LoginScreen extends StatelessWidget {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
onChanged: (v) {
if (_formKey.currentState?.validate() ?? false) {
_buttonKey.currentState?.enableButton();
} else {
_buttonKey.currentState?.disableButton();
}
},
),
);
}
}

Is it possible to position the error message below a TextFormField?

I need to fully customize the rendering of input fields and especially the way error messages are rendered. I need them to be displayed below the field without any other modification on the field.
Right now either the field is resized (default) or fixed when I set helperText: ' ' as stated in the doc, but then the field height becomes huge to keep space for the error message.
Of course I can add a Text widget below my field for the error (that's what I did actually) but then I can't use form validation as it relies on the built-in error management of the input widget to display error messages... So this would require to re-create my own form validation.
One thing I've been thinking of would be to prevent the form validation to display the error message but I couldn't find a way to do it.
Any idea?
Edit : some code bellow :
class CustomTextFormField extends StatefulWidget {
final FormFieldSetter<String> onSaved;
final ValueChanged<String> onFieldSubmitted;
final TextInputAction textInputAction;
final String initialValue;
final bool autofocus;
final bool obscureText;
final String label;
final bool withDivider;
final FocusNode focusNode;
final bool required;
CustomTextFormField(
{Key key,
this.onSaved,
this.onFieldSubmitted,
this.textInputAction,
this.initialValue,
this.autofocus = false,
this.obscureText = false,
this.label,
this.withDivider = false,
this.focusNode,
this.required})
: super(key: key);
CustomTextFormFieldState createState() => CustomTextFormFieldState();
}
class CustomTextFormFieldState extends State<CustomTextFormField> with CustomFormFieldState {
bool _isMasked = false;
String _showObscureIcon;
FocusNode _focusNode;
TextEditingController _controller;
String _errorText;
/// True if this field has any validation errors.
bool get hasError => _errorText != null;
#override
void initState() {
// Add a listener to the focusNode to detect when we loose focus
_focusNode = widget.focusNode != null ? widget.focusNode : FocusNode();
_focusNode.addListener(lostFocusListener);
// Create the TextEditingController for the field with optional initial value
_controller = TextEditingController(text: widget.initialValue);
_controller.addListener(() {
CustomForm.of(context).fieldDidChange(); // CustomForm is basically a simplified and slightly adapted version of the built-in Form
});
// Set obscure properties
if (widget.obscureText) {
_isMasked = true;
_showObscureIcon = 'assets/img/icon_displaypassword.png';
}
super.initState();
}
/// Displays the field label
Widget _inputLabel() {
return Container(
child: Text(widget.label,
style: hasError ? TextStyle(color: themeErrorColor) : null),
margin: const EdgeInsets.only(bottom: 8.0),
);
}
/// Displays the field
Widget _inputField() {
return Expanded(
child: TextFormField(
decoration: InputDecoration(
enabledBorder: themeInputBorder,
focusedBorder: themeInputBorder,
contentPadding: EdgeInsets.all(14.0),
),
textInputAction: widget.textInputAction,
autofocus: widget.autofocus,
onSaved: widget.onSaved,
onFieldSubmitted: widget.onFieldSubmitted,
obscureText: _isMasked,
focusNode: _focusNode,
controller: _controller,
));
}
/// Displays the obscured toggle button
Widget _obscureButton() {
return IconButton(
icon: Image.asset(_showObscureIcon),
onPressed: _toggleObscureText,
);
}
/// Displays the decorated field
Widget _decoratedField() {
// Row that contains the input field
List<Widget> rowChildren = <Widget>[_inputField()];
// If the field must be oscured, we add the toggle button to the row
if (widget.obscureText) {
rowChildren.add(_obscureButton());
}
return Container(
decoration: hasError ? themeInputErrorDecoration : themeInputDecoration, // themeInputErrorDecoration and themeInputDecoration are LinearGradient defined elsewhere
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(2.0)),
child: Row(
children: rowChildren,
),
));
}
/// Displays the error message if any
Widget _errorLabel() {
return Offstage(
offstage: !hasError,
child: Container(
margin: EdgeInsets.only(top: 10.0),
child: Text(_errorText == null ? '' : _errorText,
style: TextStyle(color: themeErrorColor))));
}
/// Toggles beetween obscured/clear text and action icon
void _toggleObscureText() {
setState(() {
_isMasked = !_isMasked;
_showObscureIcon = _isMasked
? 'assets/img/icon_displaypassword.png'
: 'assets/img/icon_dontdisplaypassword.png';
});
}
/// Callback called when the focus has changed. Validates the input text
void lostFocusListener() {
if (!_focusNode.hasFocus) {
if (!touched) {
touched = true;
}
}
}
/// Saves the field
void save() {
widget.onSaved(_controller.text);
}
/// Resets the field to its initial value.
void reset() {
setState(() {
_controller.text = widget.initialValue;
_errorText = null;
});
}
/// Validates the field and set the [_errorText]. Returns true if there
/// were no errors.
bool validate() {
// If the field has not been touched yet, the field is validated
if(!touched) {
return true;
}
setState(() {
_validate();
});
return !hasError;
}
void _validate() {
// TODO: write a real validator
if (_controller.text.isEmpty && widget.required) {
_errorText = 'Field is required';
} else {
_errorText = null;
}
}
#override
Widget build(BuildContext context) {
register(context);
// Widgets for the column
List<Widget> children = <Widget>[
_inputLabel(),
_decoratedField(),
_errorLabel()
];
// Adds a divider if neeed to the column
if (widget.withDivider) {
children.add(Divider(height: 40.0));
}
// Renders the column
return Column(
crossAxisAlignment: CrossAxisAlignment.start, children: children);
}
#override
void dispose() {
// Clean up the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
#override
void deactivate() {
unregister(context);
super.deactivate();
}
}
abstract class CustomFormFieldState {
String errorText;
/// True if this field has any validation errors.
bool get hasError => errorText != null;
bool touched = false;
void register(BuildContext context) {
CustomForm.of(context)?.register(this);
}
void unregister(BuildContext context) {
CustomForm.of(context)?.unregister(this);
}
void save();
void reset();
bool validate();
}

Flutter onChanged and onSaved together for Text Inputs

I've been trying to implement a small form in Flutter and found that the onChanged and onSaved events are not available together on either of the 2 TextInput widgets.
onChanged is defined in TextField widget and onSaved is defined in TextFormField widget. One workaround is to use the TextEditingController to watch for changes but that adds a bunch of additional lines of code to add listeners, remove listeners and dispose. Is there a better solution to address this issue?
You can create your own widget to support that method, like this :
import 'package:flutter/material.dart';
class MyTextField extends StatefulWidget {
final Key key;
final String initialValue;
final FocusNode focusNode;
final InputDecoration decoration;
final TextInputType keyboardType;
final TextInputAction textInputAction;
final TextStyle style;
final TextAlign textAlign;
final bool autofocus;
final bool obscureText;
final bool autocorrect;
final bool autovalidate;
final bool maxLengthEnforced;
final int maxLines;
final int maxLength;
final VoidCallback onEditingComplete;
final ValueChanged<String> onFieldSubmitted;
final FormFieldSetter<String> onSaved;
final FormFieldValidator<String> validator;
final bool enabled;
final Brightness keyboardAppearance;
final EdgeInsets scrollPadding;
final ValueChanged<String> onChanged;
MyTextField(
{this.key,
this.initialValue,
this.focusNode,
this.decoration = const InputDecoration(),
this.keyboardType = TextInputType.text,
this.textInputAction = TextInputAction.done,
this.style,
this.textAlign = TextAlign.start,
this.autofocus = false,
this.obscureText = false,
this.autocorrect = true,
this.autovalidate = false,
this.maxLengthEnforced = true,
this.maxLines = 1,
this.maxLength,
this.onEditingComplete,
this.onFieldSubmitted,
this.onSaved,
this.validator,
this.enabled,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.onChanged});
#override
_MyTextFieldState createState() => _MyTextFieldState();
}
class _MyTextFieldState extends State<MyTextField> {
final TextEditingController _controller = new TextEditingController();
_onChangedValue() {
if (widget.onChanged != null) {
widget.onChanged(_controller.text);
}
}
#override
void initState() {
_controller.addListener(_onChangedValue);
super.initState();
}
#override
void dispose() {
_controller.removeListener(_onChangedValue);
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return TextFormField(
key: widget.key,
controller: _controller,
initialValue: widget.initialValue,
focusNode: widget.focusNode,
decoration: widget.decoration,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
style: widget.style,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
autovalidate: widget.autovalidate,
maxLengthEnforced: widget.maxLengthEnforced,
maxLines: widget.maxLines,
onEditingComplete: widget.onEditingComplete,
onFieldSubmitted: widget.onFieldSubmitted,
onSaved: widget.onSaved,
validator: widget.validator,
enabled: widget.enabled,
keyboardAppearance: widget.keyboardAppearance,
scrollPadding: widget.scrollPadding,
);
}
}
And include it in your page:
Padding(
padding: EdgeInsets.all(20.0),
child: Center(child: MyTextField(
onChanged: (value) {
print("testing onchanged $value");
},
)),
)