Using StatelessWidget with method variable or Stateful widget? - flutter

I am learning Flutter and I am looking what might be the pro's / con's of some specific way of solving this. I have a simple widget which is used as a ModalBottomSheet. It will contain a form with multiple text fields to submit some data - which is returned in the callback. Example:
class SimpleBottomPopupSheet extends StatelessWidget {
final Function onSubmit;
const BottomPopupSheet({this.onSubmit});
#override
Widget build(BuildContext context) {
String text;
return Column(children: [
TextField(
textAlign: TextAlign.center,
onChanged: (value) {
text = value;
},
),
ElevatedButton(
onPressed: () {
// Do something with the text
onSubmit(text);
Navigator.pop(context);
},
child: Text("SUBMIT"),
)
]);
}
}
This seems to work, but I am wondering if this is a correct way to use it (there will be more than just one text field). It feels like it should be a stateful widget as it is keeping the state of the input.
Any best practices around this use case?

The other comments are correct. This should be a StatefulWidget as any rebuilds of the widget will cause the "text" parameter to be reset. A rather naive and basic example of how to convert your StatelessWidget into a StatefulWidget is below:
class SimpleBottomPopupSheet extends StatefulWidget {
final Function onSubmit;
const SimpleBottomPopupSheet({this.onSubmit});
#override
State<SimpleBottomPopupSheet> createState() => _SimpleBottomPopupSheetState();
}
class _SimpleBottomPopupSheetState extends State<SimpleBottomPopupSheet> {
String _text;
#override
Widget build(BuildContext context) {
return Column(children: [
TextField(
textAlign: TextAlign.center,
onChanged: (value) {
_text = value;
},
),
ElevatedButton(
onPressed: () {
// Do something with the text
widget.onSubmit(_text);
Navigator.pop(context);
},
child: const Text("SUBMIT"),
)
]);
}
}

Your widget should be a StatefulWidget. What you wrote won't work because each time the user types something the text variable will be wiped out (and other nasty things :))
You should have a look at Flutter Form tutorial: https://flutter.dev/docs/cookbook/forms/validation!
You should use StatelessWidget only when the widget does not update its values! Otherwise, you should use a StatefulWidget or more complex state management solution. Again you can find many guides on flutter.dev!
(But using a method onSubmit to return the data to the previous view/widget is a good idea)

Related

How the onChange TextField pass the data over into button onPressed property

I'm trying to pass data from TextField with onChange property to onPressed button.
If I type my string as below:
String newTextTitle;
then I get error on print(newTextTitle);:
The non-nullable local variable 'newTextTitle' must be assigned before it can be used.
Try giving it an initializer expression, or ensure that it's assigned on every execution path.
So I change it to
String? newTextTitle;
then the error won't appear again. But The data won't pass from TextField to my button, actually passing null.
And if I assigned some string then it is printing always what I assigned regardless of any change in the TextField.
My TextField code:
TextField(
autofocus: true,
textAlign: TextAlign.center,
onChanged: (newValue) {
newTextTitle = newValue;
},
),
My button code:
TextButton(
onPressed: () {
print('Passing Test $newTextTitle');
},
),
my output consol:
I/flutter (23788): Passing Test null
This code is worked so fine in older flutter.
But now I used Flutter 2.5.2 and there is somthing has been changed.
Using a TextEditingController is the Flutter recommended way of doing what your trying to do. See (https://flutter.dev/docs/cookbook/forms/text-field-changes)
// Setup your controller
final TextEditingController _controller = TextEditingController();
// Add controller to your TextField
TextField(
controller: _controller,
),
// Get the text via controller.
TextButton(
onPressed: () {
print(_controller.text);
},
child: const Text('click')
)
That's weird works completely fine for me. Maybe match the code if you wish I can leave a sample here. Make sure you are not assigning
For the onChange function make sure you are using ``TextFormField``` instead. Although it shouldn't matter in the first place something you can try as well.
It seems like that you might be assigning string within build context and within the scope of the current text field, that's mostly the reason. A sample would be a lot helpful in that case so I can re-edit my answer for your use case.
newTextTitle in the scope of the build function.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
String? newTextTitle;
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(children: [
TextFormField(
onChanged: (newValue) {
newTextTitle = newValue;
},
),
TextButton(
onPressed: () {
print(newTextTitle);
},
child: const Text('Buttom'))
]),
));
}
}
Compare the code and I am on Flutter 2.5.3
String? newTextTitle; //past it here
Buildwidget(){
String? newTextTitle; //remove from here
}

Flutter TextFormFields clear when dismissing keyboard

Pretty much what I describe in the title. I have a pile of TextFormFields populated from Firebase when the app launches.
The user should be able to update these, and when they are done, click a submit button to update the database. The database code all works, however there is some bug which works as follows:
TextFormField1: "New Text Entered"
TextFormField2: "Some more text"
TextFormField3: "Another update here"
Now we get to a point where we need to dismiss the keyboard, so that we can see the submit button underneath. As soon as you click the little down arrow to dismiss the keyboard, all the changes above revert back to their original state.
Anyone seen this?
I am prepopulating the data in these fields at runtime, and you can edit and update the text, and it all works fine... except if you minimise the keyboard.
Please tell me that Flutter isn't doing something fundamentally stupid like reloading the widget underneath from scratch every time you ask the keyboard to go away...... It sort of feels like it is.
Yes. It happens to me all the time. It is because the screen rebuilds when the bottom insets (due to keyboard) changes.
Enclose the TextFormField(s) inside a Form and give it a global key.
Use a local variable to store the value of the TextFormField. Update it in onChanged method.
All done!
I shall attach a code for easiness.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginScreen(),
);
}
}
// Login Screen
class LoginScreen extends StatefulWidget {
#override
_LoginScreenState createState() => _LoginScreenState();
static GlobalKey<FormState> _loginScreenFormKey = GlobalKey<FormState>();
}
class _LoginScreenState extends State<LoginScreen> {
String username;
String password;
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Form(
key: LoginScreen._loginScreenFormKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
hintText: 'Enter username',
),
onChanged: (value) {
setState(() {
username = value;
});
},
),
TextFormField(
decoration: InputDecoration(
hintText: 'Enter username',
),
onChanged: (value) {
setState(() {
password = value;
});
},
obscureText: true,
),
RaisedButton(
onPressed: () {
LoginScreen._loginScreenFormKey.currentState.save();
},
child: Text('submit'),
),
],
),
),
),
);
}
}
This is my solution: move the TextEditingController variable from the inside of the "build" method to the outside of the "build" method. Ref in pic The solution
The class that includes those TextFormFields should extends State of StatefulWidget, the local state will be cleared if the dismiss of keyboard causes those fields re-render, hence you need StatefulWidget to save the local state so that it won't be re-rendered
Convert you StatelessWidget to StatefulWidget.

Why do my variables are successful changed in a StatelessWidget, even tho I get a warning

This is the warning:
This class (or a class which this class inherits from) is marked as '#immutable', but one or more of its instance fields are not final: MyWidget.myVar
The console tells me that the variable must be final. I suspect that I should change my widget to a stateful if I want to change variables, but to me it doesn't makes sense, as the code works as intended. When I change my variable I don't want to change anything on the screen, I just want to use it later.
What I'm doing is wrong? If not, how can I disable this warning?
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: MyWidget(),
),
);
}
class MyWidget extends StatelessWidget {
String myVar;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
MaterialButton(
child: Text('Click me do change variable'),
onPressed: () {
myVar = 'Clicked!';
},
),
MaterialButton(
child: Text('Click me to print the variable'),
onPressed: () {
print(myVar);
},
),
],
),
);
}
}
Your logic is correct and it doesn't really matter since you are not outputting anything on screen. However the best practise is to change it to a Stateful Widget.
It doesn't really affect it in a negative way.
You are getting the warning because all fields in a class extending StatelessWidget should be final.
Fix the warning by adding the final keyword before the type declaration like below:
final String myVar;
From the documentations.
StatelessWidget class. A widget that does not require mutable state.
https://docs.flutter.io/flutter/widgets/StatelessWidget-class.html
I hope this answers your question
The point of changing variable's value in a stateful widget is that you can call
setState(() {
myVar = 'clicked';
});
Which would rebuild the UI, changing a Text widget's content.
Try adding a Text(myVar) to your column, in a stateless widget it wouldn't change on a press of a button. But in a stateful widget it will change.
If you need to change the state of a variable in a Widget, you need to use a StetefullWidget.
class MyWidget extends StatefulWidget {
final String myInitialVar;
const MyWidget({Key key, this.myInitialVar}) : super(key: key);
#override
State<StatefulWidget> createState() => MyWidgetState(myInitialVar);
}
class MyWidgetState extends State<MyWidget> {
String myVar;
MyWidgetState(this.myVar);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
MaterialButton(
child: Text('Click me do change variable'),
onPressed: () {
setState(() {
myVar = 'Clicked!';
});
},
),
MaterialButton(
child: Text('Click me to print the variable'),
onPressed: () {
print(myVar);
},
),
],
),
);
}
}

Flutter-web TextFormField issue

usually I can disable/grey-out a button until a TextFormField meets certain parameters in flutter by something like this:
TextFormField(
controller: _controller
value: (value)
)
SubmitButton(
onPressed: _controller.text.isNotEmpty ? _submit : null;
)
But when compiled as a website the Button seems no longer aware of the controller value...
I have tried targeting in several different ways, e.g. _controller.value.text.isEmpty and _controller.text.isEmpty...
I'm guessing I'm missing something or this method just isn't possible for web ... Is there any other way to get the same result?
To be honest, your code shouldn't work in flutter mobile either, but may be works because of screen keyboard causes widget rebuild when showing or hiding.
To fix this issue we have to use stateful widget with state variable like canSubmit and update it in textField's listener onChange with setState method. Then every time the text changes, our stateful widget will update the submit button..
class Page extends StatefulWidget {
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
bool canSubmit;
#override
void initState() {
canSubmit = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
onChanged: (value) {
setState(() {
canSubmit = value.isNotEmpty;
});
},
),
RaisedButton(
onPressed: canSubmit ? _submit : null,
child: Text('Submit'),
)
],
),
),
);
}
void _submit() {
print('Submitted');
}
}

The method 'setState' isn't defined for the class MyApp error in Flutter

I am getting the error The method 'setState' isn't defined for the class MyApp error in Flutter in the onSubmitted of the TextField
Code:
Widget build(BuildContext context) {
String phoneNo;
return new MaterialApp(
title: 'SchoolTrack',
theme: new ThemeData(
primaryColor: Colors.grey[50],
),
home: new Scaffold(
appBar: null,
backgroundColor: Colors.cyan[100],
body: new Container(
padding: const EdgeInsets.all(32.0),
child: new Center(
child: new TextField(
autofocus: true,
autocorrect: false,
decoration: new InputDecoration(
hintText: 'Type the phone no',
suffixIcon: new Icon(Icons.send),
suffixStyle: new TextStyle(
color: Colors.cyan[300],
)
),
onSubmitted: (String input) {
setState(() {
phoneNo = input;
});
},
),
),
),
)
);
}
I assume you are trying to setState in a stateless widget, which is immutable(unable to change).
Use a stateful widget to do so, like this:
class MainPage extends StatefulWidget{
HomePage createState()=> HomePage();
}
class HomePage extends State<MainPage>{
//Your code here
}
place
setState
inside
StatefullWidget
:) that will solve the problem
You have to call that function within a stateful widget
setState is only available inside a StatefulWidget class/subclass. You need to convert your StatelessWidget to a StatefulWidget. Simply:
Click on the StatelessWidget class and use option + return (or cmd + . if you use VS Code on macOS),
Be Sure you typed right
setState( () {} ) ;
As mentioned above, the issue stem from StatelessWidget. For people who use text editors for coding the easiest way is like following:
Replace this:
class YourClassName extends StatelessWidget {
With this:
class YourClassName extends StatefulWidget {
#override
_YourClassNameState createState() => _YourClassNameState();
}
class _YourClassNameState extends State<YourClassName> {
All other things will be the same and now you can call setState inside your class:
setState(() {
whatever you want to update goes here
});
However, it is more practical to put your setState inside your own function and trigger it from anywhere easily:
void funtionName(passing parameters) {
setState(() {
....
});
}
Now call your function easily from anywhere like: funtionName(parameter).
Keep the cursor above the StatelessWidget and press Alt + insert and click on convert to StatefulWidget, to quickly covert your StatelessWidget to StatefulWidget.
If you are using a lambda and getting 'setstate isn't referenced' be sure to type it right with the brackets:
setState(() => _counter++);
instead of:
setState() => _counter++;
setState{} is only available inside a Stateful Widget class/subclass. You need to convert your Stateless Widget to a StatefulWidget.