Go easy. I just started learning Flutter a week ago. I'm coming from ReactJS so I have a decent understanding of state management and lifecycle methods. But I'm completely new to Dart and Flutter and how it handles state.
I am writing a quick WebRTC chat application. I have a TextField I'm using to generate room names. I decided I wanted to make the labelText of the TextField, cycle through some random words, every 5 seconds, while the field is not in focus. If the field comes into focus, I stop cycling the label. I do this so that the field appears to have a pre generated random room name.
I am having trouble editing the TextField. I assume this is an issue with setState or my TextEditingController. I'm used to being able to access an input's value, so controllers are odd to me.
Here is my ChangingTextField:
import 'dart:async';
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
//
class ChangingTextField extends StatefulWidget {
final TextEditingController controller;
ChangingTextField({
Key? key,
required this.controller,
}) : super(key: key);
#override
_ChangingTextFieldState createState() => _ChangingTextFieldState();
}
class _ChangingTextFieldState extends State<ChangingTextField> {
FocusNode _focusNode = FocusNode();
Timer? _timer;
String _roomName = "example.com/";
bool _wasFocused = false;
#override
void initState() {
super.initState();
_focusNode = FocusNode();
_timer = Timer.periodic(Duration(seconds: 5), (Timer t) => _genRoomName());
}
#override
void dispose() {
_timer?.cancel();
_focusNode.dispose();
super.dispose();
}
void _requestFocus(){
if(!_wasFocused){
setState(() {
_timer?.cancel();
_wasFocused = true;
FocusScope.of(context).requestFocus(_focusNode);
});
}
}
void _genRoomName(){
WordPair wp = generateWordPairs().take(1).first;
setState(() => _roomName = "example.com/" + wp.first + "-" + wp.second );
}
#override
Widget build(BuildContext context) {
return Container(
child: TextField(
focusNode: _focusNode,
controller: widget.controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: _wasFocused ? "example.com/" : _roomName,
),
onTap: _requestFocus,
),
);
}
}
The parent widget simply passes a TextEditingController into this widget so that I can listen for changes, and (I assume) gather the value of the TextField at a later point in time.
The listener is defined like this in the parent widget:
#override
void initState() {
roomNameController.addListener(() {
setState(() {});
});
super.initState();
}
However, every time I try to change the value of the TextField, after every character that I type, the focus is broken on the ChangingTextField widget, and I must click again inside the TextField to type my next character. I am assuming this issue is because the listener calls setState in the parent widget.
In React terminology I would refer to this as a re-render. If the parent re-renders, the child goes with it, and so the app loses what knowledge it had of where in the widget tree the user was working. However, I feel that the controller needs to exist in the parent, such that, I can acquire the value of the child when needed (e.g. on a button press). Lifting state up and whatnot.
Can someone explain to me what is going on here?
I found the solution. Listening inside of the widget instead of initializing the listener in the parent component, produces the behavior you would expect.
In short, moving the following code:
#override
void initState() {
roomNameController.addListener(() {
setState(() {});
});
super.initState();
}
into the ChangingTextField widget's initState as opposed to having it in the parent's initState, resolved the problem. Best of all, the controller is still created by the parent, so the controller's text is available in the parent when the submit button is pressed.
Related
I have the following code, shown below where every time I make changes to a text field, a function gets called, in my case doSomething(). However, I assumed that since I bound the value of the text to a variable, if that variable were to get updated, the textfield text would also update. This is not happening, and my question is, what would be the simplest way to make the textfield update its text every time the corresponding variable changes.
Edit: Just to clarify, I have no problem getting the doSomething() function to update other parts of the code, I am looking for the inverse where an external variable changes the text of the textfield
import 'package:flutter/material.dart';
class DynamicTextField extends StatefulWidget {
const DynamicTextField(
{
required this.value,
Key? key})
: super(key: key);
final double value;
#override
State<DynamicTextField> createState() =>
_DynamicTextFieldState(value);
}
class _DynamicTextFieldState extends State<DynamicTextField> {
_DynamicTextFieldState(this.value);
final double value;
late TextEditingController textChanged;
#override
void initState() {
textChanged = TextEditingController(text: value.toString());
super.initState();
}
#override
Widget build(BuildContext context) {
return TextField(
controller: textChanged,
onChanged: (text) {
doSomething();
},
);
}
}
you can change the TextField value with the textChanged controller you set to that widget.
String stringVariable = "some value";
textChanged.text = stringVariable;
you can then update the state with a normal SetState(() {}), and it should update the value in TextField
Your value is declared as final. Meaning it can only be set once. Try removing the final declaration.
I also see you’re also declaring the value variable twice, instead of referencing the original value variable.
The second declaration should be in the widgets build method & should = widget.value.
this will update the text everytime DynamicTextField get rebuild.
#override
void initState() {
textChanged = TextEditingController(text: widget.value.toString());
super.initState();
}
Depends how you use the DynamicTextField in another class. The life-cycle of StatefullWidget is call the initState once the widget initialize.
I'm currently working on a Flutter app for Windows, and I try to detect keyboard inputs (like space, arrows, enter...).
For that purpose, I use RawKeyboardListener in my widget:
class _WaveformWidgetState extends State<WaveformWidget>
{
FocusNode _focusNode = FocusNode();
#override
Widget build(BuildContext context)
{
return RawKeyboardListener(
autofocus: true,
focusNode: _focusNode,
onKey: (event) {
if (event.isKeyPressed(LogicalKeyboardKey.enter))
{
print("value : enter");
}
},
child: Container(...),
);
}
}
The problem is: that widget is inside another widget, which contains a form, with multiple TextFormFields, buttons and so on.
And because of that, it seems that the TextFormFields somehow keep the focus and prevent my RawKeyboardListener from working.
So I tried by adding FocusScope.of(context).requestFocus(_focusNode); inside the build() method of that widget, but now my TextFormFields always loose focus when I click on them. Pretty sure it's a focus related problem, but I don't know how to deal with it.
So how can I properly listen to raw keyboard events in my current widget without compromising my form?
Thanks.
OK so I found 2 solutions to my problem:
With RawKeyboardListener:
Add a click listener in the widget, and when clicked, call: FocusScope.of(context).requestFocus(_focusNode);
In the onKey callback of RawKeyboardListener, also add: FocusScope.of(context).requestFocus(_focusNode);
With FocusNode (thanks to that link):
class _WaveformWidgetState extends State<WaveformWidget>
{
FocusNode _focusNode = FocusNode();
late FocusAttachment _focusAttachment;
#override
void initState()
{
super.initState();
_focusAttachment = _focusNode.attach(context, onKeyEvent: (node, event) {
if (event.logicalKey == LogicalKeyboardKey.enter)
{
print("value : enter");
}
return KeyEventResult.handled;
});
_focusNode.requestFocus();
}
#override
void dispose()
{
_focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context)
{
_focusAttachment.reparent();
return Container(...);
}
}
I personally prefer the second solution, since it seems to work very well, and also allows you to easily handle event like shift+click (check that link again for more info on that).
How am I able to get the updated value of the TextFormField without using setState((){})?
On TextFormField 's onChanged method, I am setting the class variable to the value, but normally we would need setState((){}) for the value to be updated. I am just wondering how is this working?
class ResetPasswordPage extends StatefulWidget {
const ResetPasswordPage({ Key? key }) : super(key: key);
#override
_ResetPasswordPageState createState() => _ResetPasswordPageState();
}
class _ResetPasswordPageState extends State<ResetPasswordPage> {
String _currentPassword = '';
void onChanged(){
print('current Password is: ');
print(_currentPassword); ----------->this is printing the updated value, without setstate?
}
#override
Widget build(BuildContext context) {
return Container(
TextFormField(
onChanged: (val) => {
_currentPassword = val,
onChanged()
},
),
)
}
}
The variable value does change without the need to use setState, setState is only needed to tell Flutter that the UI has to change to reflect changes in your data.
From Flutter docs:
Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
You can create Notifier -> listener, then set the listener to listen for the Notifier,so any update on the Notifier(controller)it will notify the listener(TextFormField) to update itself.
Like this:
final _controller = TextEditingController();
child: TextFormField(
controller: _controller,
decoration: //decoration,
),
when you call this _controller.text it will notify all the listeners of this TextEditingController that needs update, then you get the same result setState((){}) will give you.
I am trying to build a responsive mobile app so I found an approach were i would divide the sreen into definite number of grids and get the grid width and height and then use this width and height to size my widgets
Question:
I would definitly get my screen's size from MediaQuery.of(context) but since i will only use it once to do my calculations will my widget tree rebuild (assuming i did this calculation in my root widget) whenever a keyboard appears or not? And if it will rebuild should i do the calculations in a different place?
No, if you didn't place any set state or callback during that rebuild the widget when you open the keyboard. However, the issue can be easily resolved by putting your main widget "below" the Scaffold in a SingleChildScrollView to avoid rendering issues.
If you absolutely need to perform actions when the keyboard appears you can use a FocusNode in the textField and add a listener to it with the addListener method. By passing a function to add Listener, you can trigger a setState every time you need, causing the widget to rebuild with the new parameters.
This is a very simplified version of what I mean:
class MyWidget extends StatefulWidget{
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
FocusNode _focusNode;
int state=0;
#override
Widget build(BuildContext context) {
return Container(
height: state==0?100:200, //change the height depending on "state"
child: TextField(
focusNode: _focusNode,
),
);
}
void onFocus(){
setState(() {
//Check if the focus node is focused
if(_focusNode.hasFocus) state=1; //Change the value of the state
});
}
#override
void initState() {
super.initState();
_focusNode=FocusNode();
_focusNode.addListener(onFocus); //Here on focus will be called
}
#override
void dispose() {
super.dispose();
_focusNode.dispose();
}
}
I am looking for a better explanation on the benefit of TextEditingController over OnChanged event for a TextField.
My understanding is that onChanged's setState notifies all widgets of the change in state variable value. This way any widget (e.g. Text) can simply use the state variable and it will be notified of its changes.
My false hopes were TextEditingController would make it even simpler that I won't even need a state variable. Something like below:
import "package:flutter/material.dart";
class TestForm extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return TestFormState();
}
}
class TestFormState extends State<TestForm> {
//string myStateVariable = "";
final ctrl = TextEditingController();
#override
Widget build(BuildContext context) {
var tf = TextField(
controller: ctrl,
);
var t = Text("Current value: " + ctrl.text); // <<<<<<<<<<< false hope! doesnt work!
var x = Column(children: <Widget>[tf,t],);
return MaterialApp(home: Material(child: Scaffold(
appBar: AppBar(title: Text("Test Form"),),
body: x,
)));
}
}
Can anyone explain why TextEditingController or something similar cannot manage the state itself and notifies all consumers of change in state?
Thanks.
You are just not setting state synchronously that's all. What onChanged does is exactly possible with this approach:
class _TestFormState extends State<TestForm> {
late TextEditingController controller;
#override
void initState() {
controller = TextEditingController()
..addListener(() {
setState(() {});
});
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text('Current Value: ${controller.text}'),
TextField(
controller: controller,
),
],
);
}
}
As you see, we have listener that setting state every time state of the controller changes. This is exactly what onChanged does.
So, about benefits, you can achieve everything with both approach, it's a subjective way.
About benefits:
If you need to hold field values within Stream, onChanged is what you need. In other cases you may use controller.
Actually you won't need both in most of time in my opinion because TextFormField + Form within StatefulWidget is quite complete way to implement form pages. Checkout cookbook: https://flutter.dev/docs/cookbook/forms/validation
TextEditingController actually is managing his own state, that's why you can see the input on the screen once you change it.
You have 2 problems here, the first is that you are not adding any listener to the TextEditingController, you are just asking "give me the current value" only when you build the widget, not "give me the value any time it changes". To achieve this you need to add a listener to the text controller and it will be called every time that the value change.
Try this :
#override
void initState() {
super.initState();
// Start listening to changes.
ctrl.addListener(_printValue);
}
_printValue() {
print("Value: ${ctrl.text}");
}
This will work because print doesn't need to render anything on the screen but if you change it to return a widget it will not work either. That is the second problem, as you pointed out, your parent widget is not been rebuild when the value change, in this case you cannot avoid the setState (or other way to tell flutter that needs to rebuild the widget) when the value change because you need to rebuild the widget to view the change.
Another thing that ill like to point out is that TextEditingController is much powerful and it can be used for more things that just add notifiers to changes. For example if you want a button on other part of the screen that clear the text on a TextField you will need a TextEditingController binded to that field.
Hope it helps!