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");
},
)),
)
Related
I composed a custom text field that check the validator when it lost focus, with some UI such as error text and green checked icon. They are working fine until I need to perform some auto-filling according to other TextField's data.
When user filled up the postal code, the API will be called from ChangeNotifierProvider view model and update the state according
user.prefectureName = data.prefectureName;
user.city = data.city;
user.address = data.address;
notifyListeners();
In the HookConsumerWidget page, I passed the state into the custom text field like this
OCTextField(
text: user.prefectureName,
labelText: l10n.accountProfileEditPrefectureName,
errorText: "都道府県は必須項目です",
onChanged: (value) => user.prefectureName = value,
validator: (value) => value.isNotEmpty),
OCTextField(
text: user.city,
labelText: l10n.accountProfileEditCity,
errorText: "市区町村は必須項目です",
onChanged: (value) => user.city = value,
validator: (value) => value.isNotEmpty),
OCTextField(
text: user.address,
labelText: l10n.accountProfileEditAddress,
errorText: "丁目・番地は必須項目です",
onChanged: (value) => user.address = value,
validator: (value) => value.isNotEmpty),
However, when the state change (user.prefectureName, user.city and user.address), my custom text field won't be able to reflect the changes accordingly.
Here is part of my custom text field code
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../util/ext/string_ext.dart';
typedef Validator = bool Function(String value);
class OCTextField extends HookWidget {
const OCTextField(
{Key? key,
this.text = "",
this.labelText,
this.hintText,
this.errorText,
this.maxLines = 1,
this.maxLength,
this.sanitise = true,
this.onChanged,
this.validator})
: super(key: key);
final String text;
final String? labelText;
final String? hintText;
final String? errorText;
final int? maxLines;
final int? maxLength;
final bool sanitise;
final ValueChanged<String>? onChanged;
final Validator? validator;
#override
Widget build(BuildContext context) {
final _showError = useState(false);
final _controller = useTextEditingController(text: text);
final _checkIcon;
final unchecked =
const Icon(Icons.check_circle_outline, color: Colors.black26);
final checked = const Icon(Icons.check_circle, color: Colors.green);
if (validator != null) {
_checkIcon = useState(validator!(_controller.text) ? checked : unchecked);
} else {
_checkIcon = useState(checked);
}
return Focus(
child: TextField(
controller: _controller,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(1.0),
),
suffixIcon: _checkIcon.value,
labelText: labelText,
hintText: hintText,
errorText: _showError.value ? errorText : null,
),
maxLines: maxLines,
maxLength: maxLength,
onChanged: onChanged),
onFocusChange: (isFocused) {
if (isFocused) {
_showError.value = false;
_checkIcon.value = unchecked;
} else {
if (sanitise) {
_controller.text = _controller.text.sanitise();
onChanged!(_controller.text.sanitise());
}
if (validator != null) {
_showError.value = !validator!(_controller.text);
}
_checkIcon.value = _showError.value ? unchecked : checked;
}
});
}
}
I actually read the documentation of useTextEditingController(text: text) which it does not react to the text changes. I tried to replace useTextEditingController to TextEditingController and it somehow works, with some strange behaviour - Even I erased the reflected pre-filled text and move the another field, the text will come back again; The cursor is acting weird, when I click on the field, it starts from the beginning of the pre-fill text.
It works perfectly without strange issue when I inherited TextField class and initialise the TextControllerEditor directly into TextField instance.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class OutlinedTextField extends TextField {
OutlinedTextField(
{String? labelText,
String? text,
String? errorText,
String? hintText,
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
Icon? icon,
List<TextInputFormatter>? inputFormatters,
ValueChanged<String>? onChanged,
isDense = false})
: super(
controller: text == null ? null : TextEditingController(text: text),
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(1.0),
),
labelText: labelText,
hintText: hintText,
errorText: errorText,
prefixIcon: icon,
isDense: isDense),
keyboardType: keyboardType,
maxLines: maxLines,
inputFormatters: inputFormatters,
onChanged: onChanged,
);
}
With this approach, I am not able to obtain the controller. The reason I am refactor the Custom Text Field with composition instead of inheritance is to access the controller.text for validation UI. Am I doing some serious mistake or misunderstanding the concept here? I am not able to figure out this strange behaviour.
you have to add reusable controller in your custom textfield
Note: You have to always create a seperate controller for each textfield
class CustomTextField extends StatelessWidget {
// created custom controller
TextEditingController controller;
String text;
CustomTextField({
required this.controller,
required this.text
});
#override
Widget build(BuildContext context) {
return TextField();
}
}
then create a textEditingController for each text Filed and initilize it in oninit function.
late TextEditingController text1 ;
late TextEditingController text2 ;
use the controllers in your custom text fields
CustomTextField(controller: text1 ,text: "text1",),
CustomTextField(controller: text2 ,text: "text2",)
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,
);
}
}
I have a AppTextField in flutter app as follow:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
class AppTextField extends StatefulWidget {
final int maxLines;
final String? title;
final TextInputType? keyboardType;
final bool autoFocus;
final TextInputAction inputAction;
final bool isSuffixIcon;
AppTextField(
{this.title,
this.maxLines: 1,
this.keyboardType,
this.autoFocus: false,
this.inputAction: TextInputAction.next,
this.isSuffixIcon: false});
#override
State<StatefulWidget> createState() => AppTextFieldSate();
}
class AppTextFieldSate extends State<AppTextField> {
String? text = '';
bool isRTL(String text) {
return intl.Bidi.detectRtlDirectionality(text);
}
#override
Widget build(BuildContext context) => Container(
child: TextField(
textDirection: isRTL(text!) ? TextDirection.rtl : TextDirection.ltr,
textInputAction: widget.inputAction,
keyboardType: widget.keyboardType,
autofocus: widget.autoFocus,
style: Theme.of(context).textTheme.bodyText1,
maxLines: widget.maxLines,
decoration: InputDecoration(
labelText: widget.title,
suffixIcon: widget.isSuffixIcon
? Icon(Icons.check_circle, color: Theme.of(context).hintColor)
: Container(),
),
onChanged: (value) {
setState(() {
text = value;
});
}));
}
When I use maxLines in AppTextField, there is a problem!
AppTextField(maxLines: 5, keyboardType: TextInputType.multiline)
Only one character is entered in a line as follow picture:
My question is:
Why occur this problem and I how to resolve it?
I resolved it :)
I must use null instead of Container in suffixIcon widget.
Container widget make to problem.
suffixIcon: widget.isSuffixIcon
? Icon(Icons.check_circle, color: Theme.of(context).hintColor)
: null,
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.
I am getting some strange issue where one of TextField always gets clears if you tap on it.
class MyEditText extends StatefulWidget {
static String tag = "MyEditText";
#override
MyEditTextState createState() => MyEditTextState();
}
class MyEditTextState extends State<MyEditText> {
String results = "";
final TextEditingController controller = new TextEditingController();
final TextEditingController controller1 = new TextEditingController();
#override
Widget build(BuildContext context) {
final email = TextField(
decoration: InputDecoration(
hintText: 'Enter Email',
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0)),
);
final password = TextField(
obscureText: true,
decoration: InputDecoration(
hintText: 'Enter Password',
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
),
);
return new Scaffold(
appBar: new AppBar(
automaticallyImplyLeading: false,
title: new Text("EditText Sample"),
backgroundColor: Colors.yellow,
),
body: new Container(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[email, password],
),
),
);
}
}
I am using statful widget for it and all classes from where this screen launch also statful.
Note: If I comment out all TextEditingController and its usage, everything works fine, SO I m not getting what is wrong with TextEditingController
Thanks for the updated code.
The reason your TextEditingController get cleared is because you declare the variables inside of State<MyEditText>. When the State gets re-initialized - those variables do, too.
I can see 2 ways to solve this:
#1 - Move controllers out of the State to the parent class, passing them as arguments
Controllers are declared and maintained outside of MyEditText widget - in the parent class.
class MyEditText extends StatefulWidget {
MyEditText({ Key key, this.emailController, this.passwordController }): super(key: key);
final TextEditingController emailController;
final TextEditingController passwordController;
static String tag = "MyEditText";
#override
MyEditTextState createState() => MyEditTextState();
}
class MyEditTextState extends State<MyEditText> {
String results = "";
#override
Widget build(BuildContext context) {
// ...
TextField(
controller: widget.emailController,
// ...,
),
TextField(
controller: widget.passwordController,
// ...,
),
// ...
}
}
Then you declare controllers in your parent class and pass them as arguments to MyEditText:
final emailController = TextEditingController();
final passwordController = TextEditingController();
// ...
MyEditText(
emailController: emailController,
passwordController: passwordController,
)
#2 - Reuse controllers from the old state on didUpdateWidget call
Controllers can be declared outside of MyEditText class, but if they were not - widget creates and maintains TextEditingController on its own.
class MyEditText extends StatefulWidget {
MyEditText({ Key key, this.emailController, this.passwordController }): super(key: key);
final TextEditingController emailController;
final TextEditingController passwordController;
static String tag = "MyEditText";
#override
MyEditTextState createState() => MyEditTextState();
}
class MyEditTextState extends State<MyEditText> {
TextEditingController _emailController;
TextEditingController _passwordController;
#override
void initState() {
super.initState();
if (widget.emailController == null)
_emailController = TextEditingController();
if (widget.passwordController == null)
_passwordController = TextEditingController();
}
#override
void didUpdateWidget(MyEditText oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.emailController == null && oldWidget.emailController != null)
_emailController = TextEditingController.fromValue(oldWidget.emailController.value);
else if (widget.emailController != null && oldWidget.emailController == null)
_emailController = null;
if (widget.passwordController == null && oldWidget.passwordController != null)
_passwordController = TextEditingController.fromValue(oldWidget.passwordController.value);
else if (widget.passwordController != null && oldWidget.passwordController == null)
_passwordController = null;
}
#override
Widget build(BuildContext context) {
// ...
TextField(
controller: _emailController ?? widget.emailController,
// ...,
),
TextField(
controller: _passwordController ?? widget.passwordController,
// ...,
),
// ...
}
// ...
}
Both methods are similar except that the second one regulates State<MyEditText> variables on its own.
I will leave it to you to decide which one is more suitable in your case.
Let me know if this helped.
TextEditingController controller = TextEditingController();
TextEditingController controller1 = TextEditingController();
final email = TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
prefixIcon: Icon(Icons.person_outline, color: Colors.grey),
hintText: 'Enter Email',
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0)),
);
final password = TextField(
controller1: passwordController,
obscureText: true,
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock_open, color: Colors.grey),
hintText: 'Enter Password',
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
),
);
clearName() {
controller.text = '';
controller1.text = '';
}
//call the clearName function wherever needed
Can you try this way
TextFormField(
cursorColor: Colors.white,
autofocus: false,
keyboardType:
TextInputType.emailAddress,
controller: _textEditingControllerEmail,
),
TextFormField(
autofocus: false,
controller:_textEditingControllerPassword,
cursorColor: Colors.white,
obscureText: true,
),