How can I validate a checkbox in a Flutter Form? Every other validation works fine, but the checkbox doesn't show an Error.
Here is my code:
FormField(
validator: (value) {
if (value == false) {
return 'Required.';
}
},
builder: (FormFieldState<dynamic> field) {
return CheckboxListTile(
value: checkboxValue,
onChanged: (val) {
if (checkboxValue == false) {
setState(() {
checkboxValue = true;
});
} else if (checkboxValue == true) {
setState(() {
checkboxValue = false;
});
}
},
title: new Text(
'I agree.',
style: TextStyle(fontSize: 14.0),
),
controlAffinity: ListTileControlAffinity.leading,
activeColor: Colors.green,
);
},
),
A cleaner solution to this problem is to make a class that extends FormField<bool>
Here is how I accomplished this:
class CheckboxFormField extends FormField<bool> {
CheckboxFormField(
{Widget title,
FormFieldSetter<bool> onSaved,
FormFieldValidator<bool> validator,
bool initialValue = false,
bool autovalidate = false})
: super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<bool> state) {
return CheckboxListTile(
dense: state.hasError,
title: title,
value: state.value,
onChanged: state.didChange,
subtitle: state.hasError
? Builder(
builder: (BuildContext context) => Text(
state.errorText,
style: TextStyle(color: Theme.of(context).errorColor),
),
)
: null,
controlAffinity: ListTileControlAffinity.leading,
);
});
}
in case if you want to put your checkbox directly in your Form widget tree you can use solution provided below with FormField widget. Instead of using ListTile I used rows and columns as my form was requiring different layout.
FormField<bool>(
builder: (state) {
return Column(
children: <Widget>[
Row(
children: <Widget>[
Checkbox(
value: checkboxValue,
onChanged: (value) {
setState(() {
//save checkbox value to variable that store terms and notify form that state changed
checkboxValue = value;
state.didChange(value);
});
}),
Text('I accept terms'),
],
),
//display error in matching theme
Text(
state.errorText ?? '',
style: TextStyle(
color: Theme.of(context).errorColor,
),
)
],
);
},
//output from validation will be displayed in state.errorText (above)
validator: (value) {
if (!checkboxValue) {
return 'You need to accept terms';
} else {
return null;
}
},
),
You could try something like this :
CheckboxListTile(
value: checkboxValue,
onChanged: (val) {
setState(() => checkboxValue = val
},
subtitle: !checkboxValue
? Text(
'Required.',
style: TextStyle(color: Colors.red),
)
: null,
title: new Text(
'I agree.',
style: TextStyle(fontSize: 14.0),
),
controlAffinity: ListTileControlAffinity.leading,
activeColor: Colors.green,
);
The above answer is correct, however, if you want to display an error message that is more consistent with the default layout of a TextFormField widget error message, then wrap the Text widget in a Padding widget, and give it the hex colour #e53935.
Note: You may need to adjust the left padding to fit the CheckboxListTile widget is also wrapped in a Padding widget.
Check the code below:
bool _termsChecked = false;
CheckboxListTile(
activeColor: Theme.of(context).accentColor,
title: Text('I agree to'),
value: _termsChecked,
onChanged: (bool value) => setState(() => _termsChecked = value),
subtitle: !_termsChecked
? Padding(
padding: EdgeInsets.fromLTRB(12.0, 0, 0, 0),
child: Text('Required field', style: TextStyle(color: Color(0xFFe53935), fontSize: 12),),)
: null,
),
Related
I have layout issues because the text from the dropdown menu that you choose can't fit inside the TextFormField and I can't seem to fix this issue. I have tried adding the overflow: TextOverflow.ellipsis property but that only changes for the dropdown labels, still when I choose one, they can't fit inside the TextFormField.
When you press the dropdown button, it shows like this:
and with the TextOverflow.elipsis property:
which is fine, but I still have the layout issue because of the chosen text that is now displayed inside the textformfield:
How can I add the same property to the TextFormField, or any sort of solution to this issue?
The code:
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
decoration:
kTextFieldDecoration.copyWith(
hintText: 'Legal status',
labelText: 'Legal status',
),
value: _legalStatus,
items: [
'Sole proprietorship',
'Partnerships',
'Limited Liability Company (LLC)',
'Corporation',
'Small Business Corporation (S-Corporation)'
]
.map((label) => DropdownMenuItem(
child: Text(
label.toString(),
),
value: label,
))
.toList(),
onChanged: (value) {
setState(() {
_legalStatus = value;
print(value);
});
},
validator: (thisValue) {
if (thisValue == null) {
return 'Please choose your legal status';
}
return null;
},
),
),
SizedBox(
width: 16.0,
),
Container(
width: 120.0,
child: DropdownButtonFormField<String>(
decoration:
kTextFieldDecoration.copyWith(
hintText: 'Year established',
labelText: 'Year established',
),
value: _yearEstablished,
items: items // a list of numbers that are Strings
.map((label) => DropdownMenuItem(
child:
Text(label.toString()),
value: label,
))
.toList(),
onChanged: (value) {
setState(() {
_yearEstablished = value;
print(value);
});
},
validator: (thisValue) {
if (thisValue == null) {
return 'Please choose your company year of establishment';
}
return null;
},
),
),
],
),
Thanks in advance for your help!
You need to use isExpanded property of DropDownFormField to solve this error.
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
isExpanded: true,
decoration:
kTextFieldDecoration.copyWith(
hintText: 'Legal status',
labelText: 'Legal status',
),
value: _legalStatus,
items: [
'Sole proprietorship',
'Partnerships',
'Limited Liability Company (LLC)',
'Corporation',
'Small Business Corporation (S-Corporation)'
]
.map((label) => DropdownMenuItem(
child: Text(
label.toString(),
),
value: label,
))
.toList(),
onChanged: (value) {
setState(() {
_legalStatus = value;
print(value);
});
},
validator: (thisValue) {
if (thisValue == null) {
return 'Please choose your legal status';
}
return null;
},
),
),
SizedBox(
width: 16.0,
),
Container(
width: 120.0,
child: DropdownButtonFormField<String>(
decoration:
kTextFieldDecoration.copyWith(
hintText: 'Year established',
labelText: 'Year established',
),
value: _yearEstablished,
items: items // a list of numbers that are Strings
.map((label) => DropdownMenuItem(
child:
Text(label.toString()),
value: label,
))
.toList(),
onChanged: (value) {
setState(() {
_yearEstablished = value;
print(value);
});
},
validator: (thisValue) {
if (thisValue == null) {
return 'Please choose your company year of establishment';
}
return null;
},
),
),
],
),
You need to use selectedItemBuilder parameter which will control how the selected item will be displayed on the button. Then, TextOverflow.ellipsis will work with you as expected. Here's how to use it:
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Text(item, overflow: TextOverflow.ellipsis,);
}).toList();
},
How to show active mark on RadioListTile? I used selected and activeColor but it does not work.
RadioListTile(
selected: true,
activeColor: Theme.of(context).primaryColor, //
title: Text(AppLocalizations.key(context, 'setDark')),
value: ThemeMode.dark,
groupValue: _themeMode,
onChanged: (value) {},
),
Documentation
"groupValue → T?
The currently selected value for this group of radio buttons.
This radio button is considered selected if its value matches the groupValue.
"
The one selected is the one whose group value matches the value.
Therefore your radio buttons should have as groupValue Theme.Dark or Theme.Light and update it on onChanged.
You need to create two RadioListTile for this and will contain the same groupValue. Based on groupValue it will decide active RadioListTile.
When value match with groupValue it will show active mark status.
class _HomesState extends State<Homes> {
ThemeMode _themeMode = ThemeMode.dark;
#override
Widget build(BuildContext context) {
return Scaffold(body: LayoutBuilder(
builder: (context, constraints) {
return Center(
child: Column(
children: [
RadioListTile<ThemeMode>(
selected: true,
activeColor: Theme.of(context).primaryColor,
title: Text(
'setDark',
),
value: ThemeMode.dark,
groupValue: _themeMode,
onChanged: (value) {
setState(() {
_themeMode = value!;
});
},
),
RadioListTile<ThemeMode>(
selected: true,
activeColor: Theme.of(context).primaryColor,
title: Text(
'setLight',
),
value: ThemeMode.light,
groupValue: _themeMode,
onChanged: (value) {
setState(() {
_themeMode = value!;
});
},
),
],
),
);
},
));
}
}
Is there a way to print checkbox value in a Text widget Eg. I make two checkbox which have a value of POD and Prepaid i would like to print out the selected checkbox value on Text() Widget.
Code:-
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Checkbox(
value: prepaidCheckBoxValue,
shape: const CircleBorder(),
checkColor: Colors.white,
onChanged: (value) {
prepaidCheckBoxValue = !prepaidCheckBoxValue;
//print(prepaidCheckBoxValue);
setState(() {});
},
),
const Text(
'Prepaid',
style: TextStyle(
fontSize: 20.0,
),
),
const SizedBox(
width: 30.0,
),
Checkbox(
value: podCheckBoxValue,
shape: const CircleBorder(),
checkColor: Colors.white,
onChanged: (value) {
podCheckBoxValue = !podCheckBoxValue;
setState(() {});
//print(value);
},
),
const Text(
'Pay on Delivery',
style: TextStyle(
fontSize: 20.0,
),
),
],
),
),
My recommendation
create a enum for payment methods
enum PaymentTypes {
prepaid,
pay_on_delivery,
}
create a local variable to store selected payment method in your state class
PaymentTypes selectedPaymentType;
set the selected payment method on checkbox onChange method
Checkbox(
//prepaid checkbox
...
onChanged: (value) {
podCheckBoxValue = !podCheckBoxValue;
setState(() {
selectedPaymentType = PaymentTypes.prepaid;
});
//print(value);
},
),
Checkbox(
//pay on delivery checkbox
...
onChanged: (value) {
podCheckBoxValue = !podCheckBoxValue;
setState(() {
selectedPaymentType = PaymentTypes.pay_on_delivery;
});
//print(value);
},
),
use selectedPaymentType value on Textbox
const Text(
describeEnum(selectedPaymentType).replaceAll(RegExp('_'), ' ')
)
Or you can just use a string to store the checkbox value without enums
String selectedPaymentType;
onChanged: (value) {
podCheckBoxValue = !podCheckBoxValue;
setState(() {
selectedPaymentType = 'Pay on delivery';
});
//print(value);
},
),
const Text(
selectedPaymentType
)
I am new to Flutter and I am creating a form with TextFormfield and DropdownButtonFormField. I am trying to display the error message of the DropdownButtonFormField(Site Crop) to below the text box like how the site name field display its error message.
Should I continue using DropdownButtonFormField or is there a better way to do this?
FormSample
code for dropdown
child: DropdownButtonHideUnderline(
child: DropdownButtonFormField(
decoration: InputDecoration(
errorBorder: InputBorder.none,
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
),
hint: Text(
'Choose Country',
style: TextStyle(
fontSize: 16,
color: NewSite.black.withOpacity(0.6),
),
),
validator: (value) => value == null
? 'Please fill in' : null,
value: _selectedLocation,
onChanged: (newValue) {
setState(() {
_selectedLocation = newValue;
});
},
items: _locations.map((location) {
return DropdownMenuItem(
child: new Text(location),
value: location,
);
}).toList(),
icon: Icon(Icons.arrow_drop_down, color: ColorTheme.main,),
style: TextStyle(color: NewSite.black),
isExpanded: true,
elevation: 26,
dropdownColor: NewSite.white,
),
),
Try Form Widget,
Wrap this your filed inside Form,
like this
import 'package:flutter/material.dart';
class DemoDropDownCreate extends StatefulWidget {
#override
_DemoDropDownCreate createState() =>
_DemoDropDownCreate();
}
class _DemoDropDownCreate
extends State<DemoDropDownCreate> {
final _formKey = GlobalKey<FormState>();
bool _autovalidate = false;
String selectedCountry;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: _formKey,
autovalidate: _autovalidate,
child:
Column(
children: <Widget>[
DropdownButtonFormField<String>(
value: selectedCountry,
hint: Text(
'Select Country',
),
onChanged: (salutation) =>
setState(() => selectedCountry = salutation),
validator: (value) => value.isEmpty ? 'field required' : null,
items:
['India', 'Japan'].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
FlatButton(
child: Text('PROCEED'),
color: Colors.green,
onPressed: () {
if (_formKey.currentState.validate()) {
//form is valid, proceed further
_formKey.currentState.save();//save once fields are valid, onSaved method invoked for every form fields
print("success");
} else {
setState(() {
_autovalidate = true; //enable realtime validation
});
print("Please select all field");
}
},
)
],
),
),
);
}
}
You can indeed use validators for DropdownButtonFormField
DropdownButtonFormField(
validator: (dynamic value) => value.isEmpty ? "An error happened or please pick something" : null,
decoration: InputDecoration(....
you can change value.isEmpty to any condition you want, same as the "An error happened" message.
I'm trying to create a dynamic form so I used the idea of using a listview builder to create it. I was able to successfully create it but I faced that I cannot discard changes made to the form by popping it off after editing it. The two textFormField Job name and rate per hour were able to discard changes as they were using onsaved but on the checkbox I can't do that as it has onChanged which wraps setstate to change its state.
You can take a look at the video at this link to see how it functions as of now - https://vimeo.com/523847256
As you can see that it is retaining the data even after popping the page and coming back which I don't want it to. I'm looking for a way to prevent that and make the form the same as before if the user didn't press save.
I have tried to reassign the variables() in onpressed of back button but that didn't work. I also tried push replacement to the same page to reset it but that also didn't work. I think the cuprit here is the sublist and the initialValueTextFormField and initialValueCheckbox which are used declared under ListView.builder but I don't know how to fix that without affecting the dynamic list functionality.
class EditJobPage extends StatefulWidget {
const EditJobPage({Key key, this.job}) : super(key: key);
final Job job;
static Future<void> show(BuildContext context, {Job job}) async {
await Navigator.of(context, rootNavigator: true).pushNamed(
AppRoutes.editJobPage,
arguments: job,
);
}
#override
_EditJobPageState createState() => _EditJobPageState();
}
class _EditJobPageState extends State<EditJobPage> {
final _formKey = GlobalKey<FormState>();
String _name;
int _ratePerHour;
List<dynamic> _subList = [];
Set newSet = Set('', false);
#override
void initState() {
super.initState();
if (widget.job != null) {
_name = widget.job?.name;
_ratePerHour = widget.job?.ratePerHour;
_subList = widget.job?.subList;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 2.0,
title: Text(widget.job == null ? 'New Job' : 'Edit Job'),
leading: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: <Widget>[
FlatButton(
child: const Text(
'Save',
style: TextStyle(fontSize: 18, color: Colors.white),
),
onPressed: () => _submit(),
),
],
),
body: _buildContents(),
backgroundColor: Colors.grey[200],
);
}
Widget _buildContents() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _buildForm(),
),
),
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildFormChildren(),
),
);
}
List<Widget> _buildFormChildren() {
print(_subList);
return [
TextFormField(
decoration: const InputDecoration(labelText: 'Job name'),
keyboardAppearance: Brightness.light,
initialValue: _name,
validator: (value) =>
(value ?? '').isNotEmpty ? null : 'Name can\'t be empty',
onChanged: (value) {
setState(() {
_name = value;
});
},
),
TextFormField(
decoration: const InputDecoration(labelText: 'Rate per hour'),
keyboardAppearance: Brightness.light,
initialValue: _ratePerHour != null ? '$_ratePerHour' : null,
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: false,
),
onChanged: (value) {
setState(() {
_ratePerHour = int.tryParse(value ?? '') ?? 0;
});
},
),
Column(
children: <Widget>[
ListView.builder(
shrinkWrap: true,
itemCount: _subList?.length ?? 0,
itemBuilder: (context, index) {
String initialValueTextFormField =
_subList[index].subListTitle.toString();
bool initialValueCheckbox = _subList[index].subListStatus;
return Row(
children: [
Checkbox(
value: initialValueCheckbox,
onChanged: (bool newValue) {
setState(
() {
initialValueCheckbox = newValue;
_subList.removeAt(index);
_subList.insert(
index,
Set(initialValueTextFormField,
initialValueCheckbox));
},
);
},
),
Expanded(
child: TextFormField(
minLines: 1,
maxLines: 1,
initialValue: initialValueTextFormField,
autofocus: false,
textAlign: TextAlign.left,
onChanged: (title) {
setState(() {
initialValueTextFormField = title;
_subList.removeAt(index);
_subList.insert(
index,
Set(initialValueTextFormField,
initialValueCheckbox));
});
},
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelStyle: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
),
filled: true,
hintText: 'Write sub List here',
),
),
),
],
);
},
),
TextButton(
onPressed: () {
setState(() {
_subList.add(newSet);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.add),
Text('Add Sub Lists'),
],
),
),
],
),
];
}
void _submit() {
final isValid = _formKey.currentState.validate();
if (!isValid) {
return;
} else {
final database = context.read<FirestoreDatabase>(databaseProvider);
final id = widget.job?.id ?? documentIdFromCurrentDate();
final job = Job(
id: id,
name: _name ?? '',
ratePerHour: _ratePerHour ?? 0,
subList: _subList);
database.setJob(job);
Navigator.of(context).pop();
}
}
}
And this is the link to the full repository of the whole flutter app in case you want to look at any other part:- https://github.com/brightseagit/dynamic_forms . Thank you.
Note - This is the edited code of this repo - https://github.com/bizz84/starter_architecture_flutter_firebase.
When assigning the list we need to use _subList = List.from(widget.job.subList) instead of _subList = widget.job.subList.
Otherwise, the changes made in _subList will also be made in job.subList .