The following code builds and runs as intended--when a user types something, an error message gets shown until the string passes an email validation format.
Widget emailField(){
return StreamBuilder(
stream: bloc.emailStream,
builder: (BuildContext context, snapshot) {
return TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'E-mail address',
errorText: snapshot.error
),
onChanged: (newValue){
bloc.updateEmail(newValue);
},
);
},
);
}
I am told that when the stream changes, the builder field gets called which rebuilds the TextField. But if that is the case, shouldn't the TextField always have a blank string? What happens instead is it retains its value.
I am trying to understand what exactly is happening here. Thanks!
Not really, If you look at the code of the TextField , you'll find it's a StatefulWidget, so its has a State and the state keep the value.
class TextField extends StatefulWidget
Additionally you can use the TextEditingController to handle(get/clear/set) the data of the TextField , if you don't provide a TextEditingController it will be created by default as you can see in the source code.
#override
void initState() {
super.initState();
if (widget.controller == null)
_controller = TextEditingController();
}
You can find more information here: https://flutter.io/cookbook/forms/text-field-changes/
TextField widget is newly created, but it's corresponding element is reused/recyled (and so is it state).
Here's a pretty good explanation from the creator: https://youtu.be/AqCMFXEmf3w?t=99
Related
I'm making a flutter application where user can type a message and hit send button IN THE KEYBOARD to send the message. The problem is when I press the send button the message gets send but the keyboard gets automatically dismissed. How can I prevent that from happening?
Thanks in advance.
TextField(
autofocus: true,
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: new InputDecoration.collapsed(
hintText: "Let's talk",
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(1),
),
),
textInputAction: TextInputAction.send,
onSubmitted: null,
)
TextField widget has a parameter just for this purpose!
While onSubmit callback can be used to handle business logic when the user presses done, there is also a property called onEditingComplete, specially made to handle focus-related logic. So you should use both, for better code readability.
According to the docs:
The default implementation of onEditingComplete executes 2 different
behaviors based on the situation:
When a completion action is pressed, such as "done", "go", "send", or
"search", the user's content is submitted to the controller and then
focus is given up.
When a non-completion action is pressed, such as "next" or "previous",
the user's content is submitted to the controller, but focus is not
given up because developers may want to immediately move focus to
another input widget within onSubmitted.
Therefore, if you don't like this behaviour, for example, "send" is considered a "completion action" here, thus in an Instant Messaging (chatting) app, each time user sends a short message, the keyboard will be collapsed. But if we override the onEditingComplete callback to an empty function, it will stop the default behavior and not hide the keyboard.
Sample code:
TextField(
controller: _controller,
onSubmitted: (value) {
sendMessage(text);
_controller.clear();
},
onEditingComplete: () {}, // this prevents keyboard from closing
textInputAction: TextInputAction.send,
)
Demo:
For complete explanation and comparison between onSubmitted and onEditingComplete callbacks, check out my other answer here.
This worked for me:-
First Create a FocusNode and assign it to your textfield, do the following :-
The FocusNode is a long lived component so initialize it in the initState method:-
FocusNode inputFieldNode;
#override
void initState() {
super.initState();
inputFieldNode = FocusNode();
}
Do not forget to cleanup the FocusNode in dispose method after the Form is disposed:-
#override
void dispose() {
inputFieldNode.dispose();
super.dispose();
}
Assign the FocusNode to the textfield and write the following code in onSubmitted::-
TextField(
focusNode: inputFieldNode,
onSubmitted: (String) => FocusScope.of(context).requestFocus(inputFieldNode),
)
Now the textfield will not lose focus even after pressing the submit button.
The cleanest approach would be to use onEditingComplete() with TextEditingController instead of onSubmitted(text). Refer below example.
final _controller = TextEditingController();
TextField(
controller: _controller,
padding: EdgeInsets.all(8),
textInputAction: TextInputAction.send,
placeholder: 'Type your message',
onEditingComplete: (){
if (_controller.text.isEmpty) return;
sendMessage(_controller.text);
},
),
// Focus node
FocusNode _myTextFieldFocusNode = FocusNode();
Widget build(BuildContext context) {
return SafeArea(
body: Focus(
onFocusChange: (hasFocus) => !hasFocus ? _myTextFieldFocusNode.requestFocus() : null
Container(
child: // child goes here
)
),
);
}
If the user taps to something like it can "dismiss the keyboard" Focus() widget will detect
changes and this is where you might want to put your _myTextFieldFocusNode.requestFocus()
keyboard won't have a chance to dismiss and reopen again
SystemChannels.textInput.invokeMethod('TextInput.show');
on onSubmit method
The TextField should be changed, if the String - Variable is changed from some other method.
The text box should therefore receive an update if the user enters a new text or the associated variable has been changed from another location.
//Calling new text boxes
new eingabeTextbox(false, "Bemerkungen", "...", (String str){zahlerBemerkungen = str; print("neuer Bemerkungswert:" + str);},zahlerBemerkungen),
//paged class to avoid source code redundancy
class eingabeTextbox extends StatelessWidget {
final bool _nummerischeTastatur;
final String _ueberschrift;
final String _platzhalter;
ValueChanged<String> eingegebenerWert;
ValueChanged<String> variableUeberwachen;
eingabeTextbox(this._nummerischeTastatur, this._ueberschrift, this._platzhalter, this.eingegebenerWert, this.variableUeberwachen);
#override
Widget build (BuildContext context){
return new TextField(
keyboardType: _nummerischeTastatur == true ? TextInputType.number : TextInputType.multiline,
decoration: new InputDecoration(
labelText: _ueberschrift,
hintText: _platzhalter
),
onSubmitted: eingegebenerWert,
onChanged: variableUeberwachen
);
}
}
My approach with "ValueChanged variableUeberwachen;" does not work.
Can someone help me?
Here is my code, I've created an ExpansionTile and it has a child TextFormField.
import 'package:flutter/material.dart';
class OrderCreatePage extends StatefulWidget {
#override
_OrderCreatePageState createState() => _OrderCreatePageState();
}
class _OrderCreatePageState extends State<OrderCreatePage> {
String _userID;
TextEditingController _controllerl;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("Create"),
),
body: ExpansionTile(
title: Text("Create"),
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: "User ID",
icon: Icon(Icons.face),
),
validator: (val) {},
controller: _controllerl,
onSaved: (val) => _userID = val,
)
],
),
);
}
}
Whenever I type something into the TextFormField and collapse the ExpansionTile, the data in the TextFormField is lost. I'm using this type of UI because I have to create a big form getting a lot of details. If there is no ExpansionTile, then the user has to scroll a long way.
As mentioned by - #pskink
In your Code Add under TextEditingController _controllerl; :
#override
void initState() {
super.initState();
_controllerl = TextEditingController();
}
So your Code will look like :
class _OrderCreatePageState extends State<OrderCreatePage> {
String _userID;
TextEditingController _controllerl;
#override
void initState() {
super.initState();
_controllerl = TextEditingController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
....
......
https://api.flutter.dev/flutter/material/ExpansionTile/maintainState.html
ExpansionTile(maintainState: true, title: Text("text here"), ...
With this, the state of the children will be maintained after collapsing and opening.
I assume you use some sort of ScrollView to display the input fields.
The following is stated in the ExpansionTile reference:
This widget is typically used with ListView to create an "expand /
collapse" list entry. When used with scrolling widgets like ListView,
a unique PageStorageKey must be specified to enable the ExpansionTile
to save and restore its expanded state when it is scrolled in and out
of view.
So adding a PageStorageKey should do the trick:
ExpansionTile(
key: PageStorageKey('myInputField'),
title: Text("Create"),
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: "User ID",
icon: Icon(Icons.face),
),
validator: (val) {},
controller: _controllerl,
onSaved: (val) => _userID = val,
)
],
)
I have a TextFormField that reloads the current screen when I tap on it to enter text. When I tap on the formfield the software keyboard is displayed briefly before the entire screen reloads and renders all the widgets again. I am running the app on an Android device.
Container(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
validator: (value) {
if (value.isEmpty) {
return 'Your input cannot be empty';
}
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: RaisedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
print('validated');
}
},
child: Text('Save'),
),
),
],
),
),
margin: EdgeInsets.only(top:8.0),
),
The problem is that the controller of the TextFormField is rebuild when you click on the field, and that's the reason of your issue.
So to solve that, did you try to create a Statefull widget and then creating a TextEditingController in the State of this widget and passing it as an argument to the TextFormField ?
I had the same Problem. this was my code
class MainPage extends StatefulWidget {
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Model model = Model();
#override
Widget build(BuildContext context) {
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
var mediaWidth = MediaQuery.of(context).size.width / 2.0;
return Scaffold(
...
and I solved this problem by declaring the _formKey outside of build method. and this worked for me.
class MainPage extends StatefulWidget {
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Model model = Model();
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
var mediaWidth = MediaQuery.of(context).size.width / 2.0;
return Scaffold(
...
hope it will help you
Yes, that happens because when the keyboard appears, the flutter scaffold gets resize to the current available screen size. So, we can easily handle this by preventing the scaffold size change. I suggest to set scaffold resizeToAvoidBottomInset property false. If it's true the body and the scaffolds floating widgets should size themselves to avoid the onscreen keyboard whose height is defined by the ambient MediaQuery's, MediaQueryData,viewInsets bottom property.
Solution:
resizeToAvoidBottomInset: false,
Complete example:
#override
Widget build(BuildContext context) {
setDisplayData();
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: getAppBar(),
body: OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _buildVerticalLayout()
: _buildHorizontalLayout();
},
),
);
Check if you are using MediaQueries wrongly in your project, I had similar issue and it stopped when I changed the MediaQuery
in my case:
Size _size = MediaQuery.of(context).size;
removing this piece of code fixed my app.
When TextFormField focused the size of screen will changed because of the appearance of keyboard, that cause rebuild of state, you cant prevent re-build of state.
Instead of trying prevent re-build state, you need to solve problems which happen when state do re-build, one of common problem is declaration and initialization variables inside build(BuildContext context){ ... }' function.
The main problem, when you need to get some data related of context (like size of screen), in this case I prefer to pass this value from parent Widget...
For example this code will cause problem when re-build state:
#override
Widget build(BuildContext context) {
double? _screenHeight = MediaQuery.of(context).size.height;
return Container();
}
To solve problem get _screenHeight from parent, to know how to do that look at https://stackoverflow.com/a/50289032/2877427
I have something like this. I am having difficulty understanding this error.
Why does accessing filterController here give this error here, but it doesn't give this error if I move the current entire TextFormField creation (between comments A and B) inside the build method? How does moving the entire TextFormField inside the build method make filterController static then and resolve this issue?
class AppHomeState extends State<AppHome> with SingleTickerProviderStateMixin
{
TabController _tabController;
final filterController = new TextEditingController(text: "Search");
//----A
TextFormField email = new TextFormField(
keyboardType: TextInputType.emailAddress,
controller: filterController, ------>ERROR : Error: Only static members can be accessed in initializers
);
//----B
#override
Widget build(BuildContext context)
{
return new Scaffold(
appBar: new AppBar(..),
);
}
}
How can I resolve this issue?
class AppHomeState extends State<AppHome> with SingleTickerProviderStateMixin {
TabController _tabController;
final filterController = new TextEditingController(text: "Search");
TextFormField email = ...
... is an initializer and there is no way to access this at this point.
Initializers are executed before the constructor, but this is only allowed to be accessed after the call to the super constructor (implicit in your example) was completed.
Therefore only in the constructor body (or later) access to this is allowed.
This is why you get the error message:
controller: filterController,
accesses this.filterController (this is implicit if you don't write it explicit).
To work around your issue (assuming email needs to be final) you can use a factory constructor and a constructor initializer list:
class AppHomeState extends State<AppHome> with SingleTickerProviderStateMixin {
factory SingleTickerProviderStateMixin() =>
new SingleTickerProviderStateMixin._(new TextEditingController(text: "Search"));
SingleTickerProviderStateMixin._(TextEditingController textEditingController) :
this.filterController = textEditingController,
this.email = new TextFormField(
keyboardType: TextInputType.emailAddress,
controller: textEditingController);
TabController _tabController;
final filterController;
final TextFormField email;
or when the email field does not need to be final email can be initialized in the constructor initializer list:
class AppHomeState extends State<AppHome> with SingleTickerProviderStateMixin {
SingleTickerProviderStateMixin() {
email = new TextFormField(
keyboardType: TextInputType.emailAddress,
controller: filterController,
);
}
TabController _tabController;
final filterController = new TextEditingController(text: "Search");
TextFormField email;
but in Flutter widgets initState is usually used for that
class AppHomeState extends State<AppHome> with SingleTickerProviderStateMixin {
#override
void initState() {
super.initState();
email = new TextFormField(
keyboardType: TextInputType.emailAddress,
controller: filterController,
);
}
TabController _tabController;
final filterController = new TextEditingController(text: "Search");
TextFormField email;
You can keep that as a method:
Widget getEmailController(){
return new TextFormField(
keyboardType: TextInputType.emailAddress,
controller: filterController,
);
}
and use it in UI:
body: Container(
child: getEmailController();
)
You can convert this variable to a function and you can take context in this function parameters.
Example
Widget myDialog (BuildContext context) {
return new Scaffold(
backgroundColor: Colors.white,
body: new Center(
child: new Column(
children: <Widget>[
new Text("Invalid Username/Password"),
new Text("Please verify your login credentials"),
new RaisedButton(
child: new Text("Ok"),
onPressed:() {
Navigator.pop(context);//Error : Only static members can be accessed in initializers
}
),
],
),
)
);
}
// Using if you are doing in a class
this.myDialog(context);
// Using if you are using a global function
myDialog(context);
But, i think you want to show a error message. So, you can do it with a dialog not an page. It's more efficient because you can specify your dialog box with buttons or messages and you can use this error dialog everywhere. Let's look my global helper function for showing error messages.
void showError(BuildContext context, String error) {
showSnackBar(
context,
new Text(
'Error',
style: new TextStyle(color: Theme.of(context).errorColor),
),
content: new SingleChildScrollView(
child: new Text(error)
),
actions: <Widget>[
new FlatButton(
child: new Text(
'Ok',
style: new TextStyle(
color: Colors.white
),
),
onPressed: () {
Navigator.of(context).pop();
},
color: Theme.of(context).errorColor,
),
]
);
}
// Using in everywhere
showError(context, 'Sample Error');
I faced the same problem, and I was able to tackle the problem by setting the initial value of the TextFormField by adding the value I need to the controller's text, example:
_carPlateController.text = _initValues['carPlate'];
or
filterController.text = 'search';
I hope this helps! As it is an elegant easy solution for when using controllers.