Why is constructor of stateful widget called upon form interaction? - flutter

I have a question about flutter lifecycle.
Something strange is happening since I updated from flutter 1.7 to 1.9 yesterday. My forms that were working perfectly started to act weird.
I have a stateful widget, which if given no object instantiates a new one in its constructor, so that the user can add a new object or use this widget to edit an existing object. something like this:
final MyObject object;
MyForm() : this.object = MyObject(name: "", origin: Origin.EARTH);
MyForm.edit(this.object);
Name is a simple string and origin is an enum.
In the form state I have a global key: final _formKey = GlobalKey<FormState>();
Later in the code I have a field name, and a dropdown with the different values of the origin.
TextFormField(
initialValue: widget.object.name,
onSaved: (value) {widget.object.name = value;),
),
DropdownButtonFormField(
decoration: InputDecoration(labelText: "Origin"),
items: _originList,
value: widget._object.origin,
onChanged: (value) {
setState(() {
widget._object.origin = value;
});
},
onSaved: (value) {
widget._object.type = value;
},
),
So for instance when I change my dropdown value, to something like mars, it indeed selects mars in my object and it shows mars on the dropdown. (confirmed by debugger)
Then when I click on the name textfield to change the name of the object, the dropdown goes back to earth.
Now the really strange thing is, if I save my object... it saves it correctly ! (with mars and the name I chose)
I found out that for some reason (that I do not understand) the constructor of the form (not the form state, but the form) is called when I click on other fields of the form, which recalls my constructor MyForm() which sets up a new MyObject with the value earth...
But then why does it saves it correctly boggles my mind... If it would be consistent with this behavior it should save the object with earth selected and not mars...
Any idea on how this is happening ? (also I make it very clear nothing wrong was happening before the update to flutter 1.9 so it may be a bug in that new version...)
Also I am testing this with an iPhone simulator if that is of any help.
Thank you for any feedback !

if anyone is having the same issue as me, you can read how to fix it here: https://github.com/flutter/flutter/issues/27821
It is a temporary fix until flutter team fixes this issue on their framework !
Cheers !

Related

How run reactions on complex structures in MobX Flutter

I try to understand how MobX observes chages. For example:
I have an object called User where I store user data. It is used in multiple places using GetIt singletons. I have general app header where i am observinf user.name by doing this:
Observer(builder: (_) => Text(userStore.user.name))
and I have page with form where I am editting user info. Changing name looks like:
Observer(
builder: (_) => TextFormField(
initialValue: _userStore.user.name,
decoration: InputDecoration(
labelText: 'Name'),
onChanged: (value) => _userStore.user.name = value,
))
So theoreticlly it should not observe shanges because reffrence of User did not change. Theoretically I suppose to use ObservableMap to do this. But this works... Why?
Also when I try to add reaction on user object or on user.name reactions do not run.
So maybe someone can exmplain me how to run actions on complex structures like maps inside of map?
So at the end of the day I was able to grasp information on how MobX works.
user.name on the header was refreshing because whole page was relbuilding
user object will trigger change if referenc eof object changes
user.name will change if it will be marked as #observable

Flutter - TextFormField, onChanged doesn't appear to be updating value

I'm having an issue with the use of a TextFormField in Flutter.
The method called in the initial value returns either null or a String depending on whether an add or edit screen is being used respectively.
If it is an edit screen I'd like the String to be the initial value, and the _itemName to be the initial value if it is not changed, or the new onChanged value if it is.
Below shows two of many attempts that I've made that have presented different problems.
In this case if the user does not edit the text the _itemName ends up being null or a zero length string (I can't remember exactly which one it is):
TextFormField(
initialValue: controller.initialNameValue(),
onChanged: (val) {
setState(() => _itemName = val);
}),
In this case the onChanged method does not update the _itemName and it remains at its initial value:
TextFormField(
initialValue: _itemName = controller.initialNameValue(),
onChanged: (val) {
setState(() => _itemName = val);
}),
The _itemName is then used in an add or edit method where it is passed as a parameter into a setter for a class.
How would I go about ensuring the _itemName remains as its initial value until it's changed, and then when it is changed it updates to the new value? Thanks
After some trial and error I've realised the way of overcoming this problem seems to be to introduce:
#override
void initState() {
super.initState();
_itemName = controller.initialNameValue();
}
And then change the TextFormField to:
TextFormField(
onChanged: (val) {
setState(() => _itemName = val);
},
initialValue: controller.initialNameValue(),
);
The main reason for not introducing a separate controller here was due to the fact I had multiple form fields within the widget I was building, and this created further confusion.
The above approach appears to have solved the issues that I faced, although I understand it may not be the cleanest approach to this.

How to keep the state of a PageView that is used inside ScreenTypeLayout? (Flutter Web)

I am using a package called responsive_builder to create a web responsive UI.
My issue is that this package rebuilds the widget that corresponds to the constraints after changing window size.
This causes the loss of user data if inserted in a TextField for example. Also this causes PageView to lose its state meaning losing the current page index if the page was change earlier before adjusting window size.
Any suggestions?
I have had a very similar issue happen with a closing/opening container with a Textfield.
You need to use Shared Preferences to store that data into the memory. Shared Preferences effectively keeps that typed data in a map - which is stored in memory and can be used anytime.
Also I highly recommend using Flutter Form Builder for your text fields as it allows you to have a initialValue in your TextField and has built in Validation.
Because there is no code to go off, your Textfield should look something like:
SharedPreferences prefs = await SharedPreferences.getInstance();
String firstName = '';
_persistantText(String value) async {
firstName = await prefs.setString(value);
}
FormBuilderTextField(
attribute: "firstName",
decoration: InputDecoration(labelText: "First Name"),
validators: [
FormBuilderValidators.required(),
FormBuilderValidators.max(70),
],
initialValue: firstName
onChanged: (value){
_persistantText(value);
}
),

Create a not final widget

I wonder is it possible to declare a widget then after that edit the properties in the widget. See example below:
InputDecoration temp = new InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.white),
// ...
);
and then
temp.suffixIcon = IconButton(icon: Icons.sth);
I can't seems to get it working as it return suffixIcon is final. Any help much appreciated.
No. That is not possible (or should be avoided as much as possible).
The reason for this is, the same widget can be inserted in multiple locations of the widget tree.
For example, it's totally reasonable to do:
Widget foo = Something();
return Row(
children: [
foo,
foo,
]
);
What you're trying to do is imperative programming:
You have a program state and whenever it changes, you update all the necessary UI elements manually.
Flutter is a reactive UI framework. Semi-mathematically speaking, that means there is some state s and a build method f and when the state changes, the framework builds the new widget subtree by calling your function f(s).
More specifically, you don't want to change the concrete attributes of your child widgets—you don't even want to be able to do that because that goes against the reactive paradigm. Rather change the state of the program and tell the framework that the state changed. It will then re-render all the child widgets with the new attributes.
Don't worry about performance too much, Flutter is heavily optimized in that regard.
Widgets with changing state are StatefulWidgets in Flutter. Every StatefulWidget has a corresponding State, which can also contain mutable (non-final) attributes like these:
bool _useFirstIcon = true;
When you change the state, use the setState function in the State like this to notify the framework about the change:
void _changeTheState() {
setState(() => _useFirstIcon = false);
}
That tells the framework to re-render that subtree. Basically, it means your build method will be called when the next frame is drawn.
There, you should return different widget trees depending on the state (your _useFirstIcon attribute).
return SomeWidget(
decoration: new InputDecoration(
suffixIcon: IconButton(
icon: _useFirstIcon ? Icons.someIcon : Icons.someOtherIcon,
onPressed: _changeTheState,
),
),
);
Note that sometimes, you do want to access the state of a widget from the parent widget. That is—arguably somewhat inelegantly—achieved by providing some sort of controller to the child widget and the child widget modifies the state of the controller to make parts of its own state accessible to the parent.
This architecture is for example used in the TextField using the TextController.

What is the difference between TextFormField and TextField?

I am new to Flutter (and Dart) and when trying to build a form to edit an object I searched online for examples and tutorials, and I saw both of these used.
What is the difference between the two? Which one should I use?
If you making a Form where you require save, reset, or validate
operations- use TextFormField. Else For Simple user input capture
TextField is sufficient.
TextFormField, which integrates with the Form widget.
This is a convenience widget that wraps a TextField widget in a FormField.
A Form ancestor is not required. The Form simply makes it easier to save, reset, or validate multiple fields at once.
To use without a Form, pass a GlobalKey to the constructor and use GlobalKey.currentState to save or reset the form field.
sample:
TextFormField(
autovalidateMode: AutovalidateMode.always
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
onSaved: (String value) {
// This optional block of code can be used to run
// code when the user saves the form.
},
validator: (String value) {
return value.contains('#') ? 'Do not use the # char.' : null;
},
)
TextField, which is the underlying text field without the Form integration.
The text field calls the onChanged callback whenever the user changes the text in the field. If the user indicates that they are done typing in the field (e.g., by pressing a button on the soft keyboard), the text field calls the onSubmitted callback.
Short answer
If you don't know what you need, then use a TextField. This is the most basic Flutter widget for getting text input from a user. It's the one you should master first.
TextField
Using a TextField is an easy way to allow user input.
TextField(
decoration: InputDecoration(
hintText: 'Name'
),
);
To get the text that the user entered, you can either get notified every time there is a change like this:
TextField(
decoration: InputDecoration(
hintText: 'Name'
),
onChanged: (text) {
// do something with text
},
),
Or you can use a TextEditingController, as described here. This will give you access to the text state.
TextFormField
If you find yourself needing to validate user text input before you save it, you might consider using a TextFormField. Imagine something like this:
There are lots of validation checks that you might want to do on a username and password.
Of course, you could still just use a couple TextFields, but TextFormField has extra builtin functionality that will make your life easier. Generally, you will only use a TextFormField when you are using it inside of a Form widget (though that isn't a strict requirement).
Here is a stripped down example from the documentation:
class MyCustomForm extends StatefulWidget {
#override
MyCustomFormState createState() {
return MyCustomFormState();
}
}
class MyCustomFormState extends State<MyCustomForm> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
validator: (value) {
// validation logic
},
),
RaisedButton(
child: Text('Submit'),
onPressed: () {
if (_formKey.currentState.validate()) {
// text in form is valid
}
},
),
],
),
);
}
}
See also
Realistic Forms in Flutter — Part 1
Forms with Flutter
TextField is a simple text field. (you don't care about user input)
TextFormField is a text field to be used in a form (you care about user input).
If you don't need to validate TextField.
If you need to validate user input, use TextFormField with validator.
TextFormField vs. TextField
TextFormField returns a TextField, but wraps the TextField with extra functionality you can use through a Form and also without (such as reset, validation, save, etc.).
Generally, you want to use TextFormField unless writing boiler-plate code is your thing.
How does TextFormField work? What can it do?
TextFormField extends FormField class, (a StatefulWidget).
FormField objects do a special thing when they are instantiated: they look up the widget tree for a Form and register themselves with that Form.
After registration, these FormField widgets can be changed by that parent Form.
Form is also a StatefulWidget. It has a FormState object.
FormState can get & set data on any/allFormFields registered to it.
For example to clear the entire form we can call reset() on FormState and FormState will iterate through all child FormFields registered, resetting each FormField to its initialValue (null by default).
Validation is another common use case for putting several FormField like TextFormField inside Form/FormState. This allows multiple fields to be validated with a single validate() call on the Form.
How? FormState has a validate() method that iterates through each registered FormField and calls that FormField's validate() method. Much more convenient calling validate() once on Form than you manually keeping track of all TextField and validating each one separately with custom code.
Details
How does a TextFormField register itself with a Form?
FormField (base class of TextFormField, etc.) make a call within their build() method:
Form.of(context)?._register(this);
In English this means:
Search up my context hierarchy until we find a Form widget (if any) and call that form's register method on myself.
The ? is in case there is no Form widget parent. The _register call will only be run if there is a Form & FormState somewhere above.
How does Form.of(context)?._register(this) work?
Form & FormState sneakily use InheritedWidget.
In FormState.build() you'll see this code:
return WillPopScope(
onWillPop: widget.onWillPop,
child: _FormScope( // ← sneaky
formState: this,
generation: _generation,
child: widget.child,
),
);
Looking at _FormScope we see:
class _FormScope extends InheritedWidget
When a parent widget is an InheritedWidget, any child can find that parent using a special "find me a parent of this exact Type" method.
Here's how that "find me" method is used/exposed inside Form as a static method we can call from anywhere:
static FormState of(BuildContext context) {
final _FormScope scope = context.dependOnInheritedWidgetOfExactType<_FormScope>();
return scope?._formState;
}
The naming of that method dependOnInheritedWidgetOfExactType is a bit tricky. It'd be more readable as findInheritedWidgetOfExactType, but it does more than just find. (We can trigger rebuilds of Form through children that have registered as dependOn this FormState).
Summary
TextFormField is the deluxe package, air-conditioned, Bluetooth-connected 8-speaker stereo version of TextField. It includes a lot of common functionality you would use when accepting user-entered information.
I think this might be the most concise and simple explanation about the differences between the two.
From the material library:
TextField: A material design text field.
TextFormField: A FormField that contains a TextField.
Similarly, you can wrap FormField around any cupertino input component such as CupertinoTextField
Below is an example about a custom CheckboxFormField, which is a FormField that wraps around the material design component Checkbox:
// A custom CheckboxFormField, which is similar to the built-in TextFormField
bool agreedToTerms = false;
FormField(
initialValue: false,
validator: (value) {
if (value == false) {
return 'You must agree to the terms of service.';
}
return null;
},
builder: (FormFieldState formFieldState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Checkbox(
value: agreedToTerms,
onChanged: (value) {
// When the value of the checkbox changes,
// update the FormFieldState so the form is
// re-validated.
formFieldState.didChange(value);
setState(() {
agreedToTerms = value;
});
},
),
Text(
'I agree to the terms of service.',
style: Theme.of(context).textTheme.subtitle1,
),
],
),
if (!formFieldState.isValid)
Text(
formFieldState.errorText ?? "",
style: Theme.of(context)
.textTheme
.caption
.copyWith(color: Theme.of(context).errorColor),
),
],
);
},
),
Rule of thumb: If your box only have a single input field, just use the raw material input like TextField (FormField is a bit overkill in this case though). If your box has many input fields, you need to wrap each one of them in a FormField, and then integrate all of them to the Form widget to reap the benefits of validating and saving all form fields at once.
Extra tip: If u have a TextField wrapped in a FormField that doesn't allow the user to enter any text such as a CupertinoPickerFormField or a SimpleDialogFormField that offers the user a choice between several options (which is basically a material SimpleDialog widget wrapped in a FormField), just simply use the hintText param of InputDecoration without using TextEditingController to manipulate text. Make the hint text have the same color as the normal input text with hintStyle: const TextStyle(color: Color(0xdd000000)).
This video from Flutter Europe will help you master forms in Flutter in no time.