What i am trying is to replicate "setText" in flutter without TextEditingController. Since I'm using FutureBuilder, i cannot go with manually updating state, thereby removing the choice of TextEditingController.
Bellow is the code for better understanding.
class DynamicFormBuilder {
GlobalKey<FormBuilderState> dynamicFormKey = GlobalKey<FormBuilderState>();
String getVariableData(String variable) {
return dynamicFormKey.currentState.fields[variable].currentState.value;
}
void setVariableData(String variable, String value) {
dynamicFormKey.currentState.fields[variable].currentState.didChange(value);
}
Widget workOnWidget(DynamicFormResponse widget) {
switch (widget.widget) {
case "text":
return Text(
widget.displayText,
style: TextStyle(
fontWeight: widget.style.fontWeight == "bold"
? FontWeight.bold
: FontWeight.normal,
fontSize: double.parse(widget.style.fontSize)),
);
case "editable":
return FormBuilderTextField(
attribute: widget.variable,
decoration: InputDecoration(labelText: widget.displayText),
onChanged: (val) {
if (widget.onChanged != null) {
onChangedFunctionMapper[widget.variable](val);
}
},
);
case "datepicker":
return FormBuilderDateTimePicker(
attribute: widget.variable,
inputType: InputType.date,
format: DateFormat("dd/MM/yyyy"),
decoration: InputDecoration(labelText: widget.displayText),
validators: [
FormBuilderValidators.required(),
],
);
case "divider":
return Container(
margin: EdgeInsets.fromLTRB(5, 10, 5, 10),
width: double.infinity,
height: 10,
color: accentColor,
);
case "dropdown":
return FormBuilderDropdown(
attribute: widget.variable,
items: convertDropdown(widget.options),
hint: Text(widget.hint),
decoration: InputDecoration(labelText: widget.displayText),
);
case "radioButton":
return FormBuilderRadioButton(
displayText: widget.displayText,
attribute: widget.variable,
isHorizontal: widget.isHorizontal,
onChanged: (val) {
if (val == "yes") {
visibilityMap["age"] = true;
} else {
visibilityMap["age"] = false;
}
},
options: widget.options
.map((lang) => FormBuilderFieldOption(value: lang))
.toList(growable: false),
);
default:
return Text("lol");
}
}
Widget buildForms(BuildContext context, Future<dynamic> fetchJSON) {
jsonData = new List();
dynamicFormKey = GlobalKey<FormBuilderState>();
accentColor = Theme.of(context).accentColor;
return FutureBuilder(
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.none &&
snapshot.hasData == null) {
return Container(
child: Text("No Data got loaded"),
);
} else if (snapshot.connectionState == ConnectionState.done) {
for (int i = 0; i < snapshot.data.length; i++) {
jsonData.add(DynamicFormResponse.fromJson(snapshot.data[i]));
}
return ListView(
padding: EdgeInsets.all(10),
children: <Widget>[
FormBuilder(
key: dynamicFormKey,
autovalidate: false,
child: Card(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: jsonData.map<Widget>((widget) {
print(widget.widget);
return workOnWidget(widget);
}).toList(),
),
),
),
),
],
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
future: fetchJSON,
);
}
}
So here when i do updates using setVariableData(variable, data), the variable in state is getting updated, but the same is not getting reflected in TextFields.
First of all, your question is quite confusing. However, I guess, you are looking for something to set the value to some xyz variable when you submit the data, right?
You can do this by two ways, these are:
Using onSubmitted Property: This gives the value when the user is done editing the value in the textfield
Using TextEditingController: Controls the text being edited.
Code using TextEditingController:
TextEditingController _controller = new TextEditingController();
TextField(
controller: _controller
)
//suppose you have a button which will print the data of the textfield for on onPressed
RaisedButton(
onPressed: () {
//this will print the value you've entered into the textfield
print(this._controller.text)
//change the value using setState here
setState((){
value_to_be_changed_variable = this._controller.text
})
},
child: const Text('Submit', style: TextStyle(fontSize: 20))
)
Code using onSubmitted Property:
TextField(
onSubmitted: (value) {
//do the operation here with the help of setState()
setState((){
variable_value_to_be_changed = value
})
}
)
I hope that'd suffice your needs. For more info read about Textfield Flutter Class thoroughly. It will help in you in many ways
Related
I am trying to use the SwitchTileList to show all my categories and toggle them on/off however it seems to either not change state/toggle or it will toggle all of them together.
At the moment the code below the showdefault items are on as should be and the rest are off, however it will not toggle any of them at the moment.
return FutureBuilder(
future: amenityCategories,
builder:
(BuildContext context, AsyncSnapshot<AmenityCategories> snapshot) {
if (snapshot.hasData) {
return ListView(
padding: EdgeInsets.zero,
children: [
SizedBox(
height: 85.0,
child: DrawerHeader(
child: Text(
'Show/Hide Map Pins',
style: new TextStyle(fontSize: 18.0, color: Colors.white),
),
decoration: const BoxDecoration(
color: Colors.green,
),
),
),
SizedBox(
height: double.maxFinite,
child: ListView.builder(
itemCount: snapshot.data!.categories.length,
itemBuilder: (context, index) {
bool toggle = false;
if (snapshot.data!.categories[index].showbydefault == 1) {
toggle = true;
}
return SwitchListTile(
title: Text(
snapshot.data!.categories[index].categoryname),
value: toggle,
onChanged: (bool val) {
if (val != toggle) {
setState(() {
toggle = !toggle;
});
}
});
},
),
),
],
);
}
return Container();
});
}
You must use a separate variable for each individual ListTile. Give your category an additional variable isActive and work with it.
onChanged: (bool val) {
if (val != snapshot.data!.categories[index].isActive) {
setState(() {
snapshot.data!.categories[index].isActive = !snapshot.data!.categories[index].isActive;
});
}
Creating a demo for setting submit button disabled until all are required TextField is not empty...
username and password TextField are empty.. then submit button should be disabled...
I have done with my basic way, but looking for advanced code so that it can be not repeated typing like I have more text fields
here is my basic code...
class _Stack4State extends State<Stack4> {
TextEditingController txtusername = TextEditingController();
TextEditingController txtpassword = TextEditingController();
bool isenable = false;
void checkfieldvalue(String username, String password) {
if (username.length > 3 && password.length > 6) {
setState(() {
isenable = true;
});
} else {
setState(() {
isenable = false;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[300],
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
const SizedBox(
height: 20,
),
TextField(
controller: txtusername,
onChanged: (value) {
checkfieldvalue(txtusername.text, txtpassword.text);
},
),
SizedBox(
height: 20,
),
TextField(
controller: txtpassword,
onChanged: (value) {
checkfieldvalue(txtusername.text, txtpassword.text);
}),
const SizedBox(
height: 20,
),
ElevatedButton(
child: isenable ? Text('Register') : Text('Fill Data First'),
onPressed: () {
if (isenable == true) {
//code for submit
}
},
),
]),
),
),
);
}
}
First define this variable:
final _formKey = GlobalKey<FormState>();
then use Form widget inside your widget tree like this:
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Form(
key: _formKey,
child: Column(
children: [
const SizedBox(
height: 20,
),
TextField(
controller: txtusername,
onChanged: (value) {
checkfieldvalue(
txtusername.text, txtpassword.text);
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'username is empty';
}else if (value.characters.length < 4){
return 'username is in wrong format';
}
return null;
},
),
SizedBox(
height: 20,
),
TextField(
controller: txtpassword,
onChanged: (value) {
checkfieldvalue(
txtusername.text, txtpassword.text);
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'password is empty';
}else if (value.characters.length < 4){
return 'password is in wrong format';
}
return null;
},
),
const SizedBox(
height: 20,
),
],
)),
ElevatedButton(
child:
Text('Register'),
onPressed: _formKey.currentState != null &&
_formKey.currentState.validate()
? () {}
: null,
),
]),
),
),
and use its key to handle validation state. You can set your checkfieldvalue in validator.
You can addListener
#override
void initState() {
super.initState();
txtusername.addListener(() {
checkfieldvalue(txtusername.text, txtpassword.text);
setState(() {});
});
txtpassword.addListener(() {
checkfieldvalue(txtusername.text, txtpassword.text);
setState(() {});
});
}
And button
ElevatedButton(
child: isenable ? Text('Register') : Text('Fill Data First'),
onPressed: isenable
? () {
if (isenable == true) {
//code for submit
}
}
: null,
),
I have a Category TextField with a controller. The controller value is updated onChange of a Product Dropdown. What I expect is upon onChange the value of the categoryField should be updated. However, I can only see the update on the TextField once I hover on it.
Category TextField
var productCategory = Column(
children: [
TextField(
controller: categoryController,
enabled: false,
focusNode: categoryFocusNode,
),
const Padding(
padding: EdgeInsets.all(5.0), child: SizedBox(width: 200)),
],
);
Product Dropdown onChange
void onChange<Product>(prod) {
BlocProvider.of<InvoiceCubit>(context).updateProduct(prod);
categoryController.text = prod.category.categoryName.toString();
}
I have finally figured it out. Since I am using a rxdart stream, I created both the Product dropdown and Category on the StreamBuilder. Then I created the categoryController within the builder itself with text value from the product category. Below is my code:
var productDropdownField = StreamBuilder<Product>(
stream: BlocProvider.of<InvoiceCubit>(context).productStream,
builder: (context, snapshot) {
final categoryController = TextEditingController();
categoryController.text = snapshot.hasData
? snapshot.data!.category.categoryName
: "";
var productCategory = Column(
children: [
CustomTextField(
labelText: "Category",
controller: categoryController,
enabled: false,
),
const Padding(
padding: EdgeInsets.all(10.0),
child: SizedBox(width: 200)),
],
);
return StreamBuilder<Object>(
stream:
BlocProvider.of<InvoiceCubit>(context).priceStream,
builder: (context, snapshot) {
return Column(
children: [
BlocBuilder<ProductsCubit, ProductsState>(
builder: (context, state) {
if (state is ProductsLoaded) {
List<DropdownMenuItem<Product>>
dropDownItems = state.products
.map((e) => DropdownMenuItem<Product>(
value: e,
child: Text(
e.productName,
style: const TextStyle(
color: Colors.black,
fontWeight:
FontWeight.w900),
),
))
.toList();
if (invoiceItem == null &&
prodSelected == false) {
onChange<Product>(state.products.first);
prodSelected = true;
}
return CustomDropdown<Product>(
labelText: "Product",
value:
BlocProvider.of<InvoiceCubit>(context)
.getProduct(),
items: dropDownItems,
context: context,
onChanged: onChange,
);
}
return const SizedBox(
width: 100, child: Text("Error"));
},
),
productCategory,
],
);
});
});
categoryController won't be updated via onChange but will be updated based on the snapshot.data
In a 'Edit Profile' page, when a Textformfield is focused on to edit the text, the entire page reloads and doesn't allow me to change or input anything. The code is like this :
body: Form(
key: _formKey,
child: FutureBuilder<DocumentSnapshot<Map<String, dynamic>>>(
future: FirebaseFirestore.instance
.collection('appusers')
.doc(widget.id)
.get(),
builder: (_, snapshot) {
if (snapshot.hasError) {
print('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
var data = snapshot.data!.data();
var name = data!['name'];
return Padding(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30),
child: Column(
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
initialValue: name,
autofocus: false,
onChanged: (value) => name = value,
decoration: InputDecoration(
labelText: 'Name: ',
border: OutlineInputBorder(),
errorStyle:
TextStyle(color: Colors.redAccent, fontSize: 12),
),
validator: (value) {
RegExp regex = new RegExp(r'^.{3,}$');
if (value == null || value.isEmpty) {
return 'Please enter full name.';
}
if (!regex.hasMatch(value)) {
return ("Please enter a name.");
}
return null;
},
),
),
SizedBox(height: 40),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
updateUser(widget.id, name);
}
Navigator.of(context).pop();
},
child: Text(
'Update',
style: TextStyle(fontSize: 15),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: TextStyle(fontSize: 15),
),
),
],
),
)
],
),
);
},
)),
Is there a reason why the page keeps being refreshed? There are other textfields similar to 'name' and clicking on any of those causes this page to reload. The formkey has been declared before Widget build as final _formKey = GlobalKey<FormState>();.
To give you an understanding of how flutter handles the app when the keyboard is opened.
When the keyboard is opened, flutter actually changes the screen size (i.e the bottom padding changes) because of this when you access the MediaQuery.of(context) in this widget it will cause the widget to rebuild so that MediaQuery.of(context) will return the updated MediaQueryData. This rebuilding is fine.
The problem is in the way you are using the FutureBuilder s future parameter. The FirebaseFirestore.instance.collection('appusers').doc(widget.id).get() will get executed everytime there is a rebuild of this EditProfile widget , but you probably want this to be called only once when the widget is loaded first time. So you need to initialize the future in the StatefulWidget initState like:
Future future;
#override
void initState() {
super.initState();
future = FirebaseFirestore.instance.collection('appusers').doc(widget.id).get();
}
Now this will not call the FirebaseFirestore call whenever the widget rebuilds. But there is another issue with your TextFormFields initialValue parameter. It uses the name variable which is declared and being initialized within the build method which does not seem right. You could do something like:
var name:
Future future;
#override
void initState() {
super.initState();
future = FirebaseFirestore.instance.collection('appusers').doc(widget.id).get();
future.then((value) {
if(value != null) {
var name = value.data!['name'];
}
});
}
I have a FORM withe 32 TEXTFORMFIELDS and abutton to validate and navigate to output screen.
TextButton(
onPressed: () {
_formKey.currentState.save(); // I added this line which does nothing
if (_formKey.currentState.validate()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return OutputScreen();
},
),
);
} else {
_showAlert(context); // third partypackage alert function
}
},
child: Text(
'Calculate',
),
),
My problem is that if the user clicked submit, sometimes it resets all fields whether the validation was true or false;
Here is a thing I found after a lot of testing, when the user inputs a text field, the input keyboard has a "DONE" key, which the user can use to end editing. Now if the user didn't use it and jus tapped on the next field for input and continues to click the subit button I created, it resets all form.
Any ideas?
or is there a way to force user to click 'DONE' on the keyboard once finished input.
Thanks in advance.
///////////////////////////////////////////////////
EDIT: Full code sample
///////////////////////////////////////////////////
class CementInputPage extends StatefulWidget {
#override
_CementInputPageState createState() => _CementInputPageState();
}
class _CementInputPageState extends State<CementInputPage> {
static const double sizedBoxHeight = 8;
final _formKeyCement = GlobalKey<FormState>();
CementData cementData = new CementData();
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFDDDDDD),
appBar: AppBar(
title: Text("Cement Input Data"),
),
body: SafeArea(
child: Form(
key: _formKeyCement,
child: ListView(
children: [
// Header data
WhiteBoxContainer( //container I created in another file
boxTitle: 'Header',
childWidget: Column(
children: [
//TODO: Validate date format
myStringTextInputRow('Date'),
SizedBox(height: sizedBoxHeight),
myStringTextInputRow('Operator Company'),
SizedBox(height: sizedBoxHeight),
myStringTextInputRow('Cement Contractor'),
SizedBox(height: sizedBoxHeight),
myStringTextInputRow('Well Name'),
SizedBox(height: sizedBoxHeight),
myStringTextInputRow('Field Name'),
SizedBox(height: sizedBoxHeight),
myStringTextInputRow('Rig'),
SizedBox(height: sizedBoxHeight),
myStringTextInputRow('Rig Supervisor'),
SizedBox(height: sizedBoxHeight),
myStringTextInputRow('Cement Supervisor'),
SizedBox(height: sizedBoxHeight),
],
),
),
//
//Well data
WhiteBoxContainer(
boxTitle: 'Well Data',
childWidget: Column(
children: [
Row(
children: [
Expanded(
child: TextFormField(
keyboardType:
TextInputType.numberWithOptions(decimal: true),
autovalidateMode:
AutovalidateMode.onUserInteraction,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Section Depth (ft)",
),
validator: (value) {
return sectionTDValidator(value);
},
),
),
checkInputStatus(InputValidationStatus.notValid),
],
),
SizedBox(height: sizedBoxHeight),
Row(
children: [
Expanded(
child: TextFormField(
keyboardType:
TextInputType.numberWithOptions(decimal: true),
autovalidateMode:
AutovalidateMode.onUserInteraction,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Casing length (ft)",
),
validator: (value) {
return casingLengthValidator(value);
},
),
),
checkInputStatus(InputValidationStatus.notValid),
],
),
Container(
child: TextButton(
onPressed: () {
_formKeyCement.currentState.save(); // line I added which did nothing
if (_formKeyCement.currentState.validate()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CementOutputScreen();
},
),
);
} else {
//I want to show an alert dialog
child: Text(
'Calculate',
),
),
),
],
),
),
),
);
}
String sectionTDValidator(String value) {
if (value.isNotEmpty) {
cementData.sectionTD = double.parse(value);
if (cementData.sectionTD < kMinWellDepth) {
return 'Section Depth is too short!';
} else if (cementData.sectionTD > kMaxWellDepth) {
return 'Section depth is too long!';
} else if (cementData.casingLength != null &&
cementData.casingLength > cementData.sectionTD) {
return 'Section depth is shorter than casing length!';
}
} else {
cementData.sectionTD = null;
}
return null;
}
String casingLengthValidator(String value) {
if (value.isNotEmpty) {
cementData.casingLength = double.parse(value);
if (cementData.casingLength < kMinWellDepth) {
return 'Casing Length is too short!';
} else if (cementData.casingLength > kMaxWellDepth) {
return 'Casing Length is too long!';
} else if (cementData.sectionTD != null &&
cementData.casingLength > cementData.sectionTD) {
return 'Casing Length is longer than section depth!';
} else if (cementData.leadLength != null) {
if (cementData.leadLength > cementData.casingLength) {
return 'Casing length is shorter than lead length!';
} else if (cementData.tailLength != null) {
// check tail length
if (cementData.tailLength > cementData.casingLength) {
return 'Casing length is shorter than lead length!';
} else if ((cementData.leadLength + cementData.tailLength) >
cementData.casingLength) {
return 'Casing length is shorter than total cement length!';
}
}
} else if (cementData.shoeTrackLength != null &&
cementData.shoeTrackLength >= cementData.casingLength) {
return 'Shoe track length is >= casing length!';
}
} else {
cementData.casingLength = null;
}
return null;
}
String shoeTrackLengthValidator(String value) {
if (value.isNotEmpty) {
cementData.shoeTrackLength = double.parse(value);
if (cementData.casingLength != null &&
cementData.shoeTrackLength >= cementData.casingLength) {
return 'Shoe track length is >= casing length!';
}
} else {
cementData.shoeTrackLength = null;
}
return null;
}
String stickUpLengthValidator(String value) {
if (value.isNotEmpty) {
cementData.stickUpLength = double.parse(value);
if (cementData.stickUpLength > kMaxStickUpLength) {
return 'Stick up length is too long!';
}
} else {
cementData.stickUpLength = null;
}
return null;
}
}
Row myStringTextInputRow(String labelText) {
return Row(
children: [
Expanded(
child: TextFormField(
keyboardType: TextInputType.name,
maxLines: 2,
minLines: 1,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: labelText,
),
),
),
checkInputStatus(InputValidationStatus.notCritical),
],
);
}
Container checkInputStatus(InputValidationStatus inputStatus) {
if (inputStatus == InputValidationStatus.notValid) {
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 3),
child: Icon(
Icons.clear,
color: Colors.red[900],
size: 18.0,
semanticLabel: 'Feedback icon',
),
);
} else if (inputStatus == InputValidationStatus.valid) {
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 3),
child: Icon(
Icons.done,
color: Colors.green[900],
size: 18.0,
semanticLabel: 'Feedback icon',
),
);
} else {
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 3),
child: Icon(
Icons.flaky,
color: Colors.orange[700],
size: 18.0,
semanticLabel: 'Feedback icon',
),
);
}
;
}
Try adding TextEditingController to yours TextFormFields and removing the autovalidateMode.
This might be because you are using ListView to render your children. ListView only renders the visible children (has recycling nature). Instead, use Column with SingleChildScrollView.
SingleChildScrollView(child:Column(children:yourFormChildren));
Also check: flutter form data disappears when I scroll