Flutter radio buttons do not respond to clicks - flutter

I have this Flutter code with two radio buttons in a FormField.
import 'package:flutter/material.dart';
enum Gender { male, female }
class RadioFormField extends FormField<String> {
RadioFormField({
FormFieldSetter<String>? onSaved,
FormFieldValidator<String>? validator,
String initialValue = '',
AutovalidateMode autovalidateMode = AutovalidateMode.always,
}) : super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
autovalidateMode: autovalidateMode,
builder: (FormFieldState<String> state) {
String maleRadioButtonTitle = 'Male';
String femaleRadioButtonTitle = 'Female';
String? _genderValue = 'male';
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: RadioListTile<String>(
title: Text(maleRadioButtonTitle),
value: Gender.male.toString(),
groupValue: _genderValue,
onChanged: (String? value) {
state.didChange(_genderValue = 'male');
},
),
),
Expanded(
child: RadioListTile<String>(
title: Text(femaleRadioButtonTitle),
value: Gender.female.toString(),
groupValue: _genderValue,
onChanged: (String? value) {
state.didChange(_genderValue = 'female');
},
),
),
],
);
},
);
}
The problem is after they have rendered none of them respond to a selection. I can not seem to understand what is wrong. What am I missing?

A few things to be fixed here.
First,
state.didChange is a function that takes a single parameter value which is used to update the state of the form with the value provided.
So, call it like this,
onChanged: (String value) {
state.didChange(value);
},
Next, you are always sending male as the groupValue with this groupValue: _genderValue. Since _genderValue is hardcoded to be male all the time.
So change it to this,
groupValue: state.value
Finally, your code will be,
class RadioFormField extends FormField<String> {
RadioFormField({
FormFieldSetter<String>? onSaved,
FormFieldValidator<String>? validator,
String initialValue = '',
AutovalidateMode autovalidateMode = AutovalidateMode.always,
}) : super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
autovalidateMode: autovalidateMode,
builder: (FormFieldState<String> state) {
String maleRadioButtonTitle = 'Male';
String femaleRadioButtonTitle = 'Female';
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: RadioListTile<String>(
title: Text(maleRadioButtonTitle),
value: Gender.male.toString(),
groupValue: state.value,
onChanged: (String? value) {
state.didChange(value);
},
),
),
Expanded(
child: RadioListTile<String>(
title: Text(femaleRadioButtonTitle),
value: Gender.female.toString(),
groupValue: state.value,
onChanged: (String? value) {
state.didChange(value);
},
),
),
],
);
},
);
}

Related

Flutter: Radio and Switch buttons isn't updating inside a listview.builder, but it works outside

I'm using a form to create multiples vehicle entries.
every time I click on the floating button it adds a vehicleform to the page one on top of the other. "A List type that receives widgets"
If I try to select the "leased radio option", the form doesn't update, and if hit the switch button right below, nothing happens again. BUT! Curiously if I hit any of the Dropdown State....it works for them (only the dropdown gets updated). and BUT! number 2: If I hit the floating button to add a new vehicle form, the changes made on the previous form gets carried to the new form. My theory is that the buttons are working under the hood, but the setStates are no running correctly
On the main_page.dart there is a stateful widget that calls vehicles_page() which holds all the scaffold and widgets for that form including a dropdown list which is called from a 3rd file(dropdown_forms.dart).
To guide towards the right direction, just lay your eyes at the end of the code on the build() function.
FYI - Flutter Doctor -v returned no errors
Yes -I'm using stateful widgets
Yes - I'm using setstates to update
No - I'm not a professional programmer, I'm a super beginner on flutter and this is my 3rd week playing with flutter
After running some tests.... if I remove them from the listview widget, it works fine.
main.dart
This is the main file
import 'package:flutter/material.dart';
import 'package:learningflutter/screens/parties_page.dart';
import 'package:learningflutter/screens/vehicles_page.dart';
import 'package:learningflutter/screens/incident_page.dart';
// import 'package:learningflutter/parties.dart';
enum VehicleType { pov, leased, pgti }
void main(List<String> args) {
runApp(const VortexEHS());
}
class VortexEHS extends StatelessWidget {
const VortexEHS({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'VortexEHS',
theme: ThemeData(
primarySwatch: Colors.teal,
),
home: _MainPage(),
);
}
}
class _MainPage extends StatefulWidget {
_MainPage({Key? key}) : super(key: key);
#override
State<_MainPage> createState() => __MainPageState();
}
class __MainPageState extends State<_MainPage> {
// int currentPageIndex = 1;
final screens = [
PartiesPage(),
VehiclesPage(),
FormIncident(),
FormIncident()
];
#override
Widget build(BuildContext context) {
return Container(child: VehiclesPage());
}
}
vehicles_page.dart (Code Below)
// ignore_for_file: unused_element, unused_field
import 'package:flutter/material.dart';
import '../dropdown_forms.dart';
enum VehicleType { pov, leased, pgti }
class VehiclesPage extends StatefulWidget {
const VehiclesPage({Key? key}) : super(key: key);
#override
State<VehiclesPage> createState() => _VehiclesPageState();
}
class _VehiclesPageState extends State<VehiclesPage> {
//Variables
VehicleType vehicleType = VehicleType.pov;
bool isCommercial = false;
String? _make;
String? _model;
String? _year;
String? _color;
String? _vimNumber;
String? _plate;
String? _ownerName;
String? _ownerAddress;
String? _ownerCity;
String? _ownerState;
String? _ownerZip;
String? _ownerPhone;
String? _insuranceCoName;
String? _insuranceCoPhone;
String? _policyHolderName;
String? _policyNumber;
List<Widget> vehicles = [];
Widget buildTypeVeihicle() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ListTile(
horizontalTitleGap: 0,
dense: true,
title: const Text('POV'),
leading: Radio(
value: VehicleType.pov,
groupValue: vehicleType,
onChanged: (VehicleType? value) {
setState(() {
vehicleType = value!;
});
}),
),
),
Expanded(
child: ListTile(
horizontalTitleGap: 0,
dense: true,
title: const Text('Leased'),
leading: Radio(
value: VehicleType.leased,
groupValue: vehicleType,
onChanged: (VehicleType? value) {
setState(() {
vehicleType = value!;
});
}),
),
),
Expanded(
child: ListTile(
horizontalTitleGap: 0,
dense: true,
title: const Text(
'PGTI',
softWrap: false,
),
leading: Radio(
value: VehicleType.pgti,
groupValue: vehicleType,
onChanged: (VehicleType? value) {
setState(() {
vehicleType = value!;
});
}),
),
),
],
);
}
Widget _buildIsCommercial() {
return SwitchListTile(
title: const Text('This is commercial vehicle?'),
value: isCommercial,
onChanged: (bool value) {
setState(() {
isCommercial = value;
});
},
// secondary: const Icon(Icons.car_repair),
);
}
Widget _buildVehicleMake() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Make'),
validator: (String? value) {
if (value == null) {
return 'Make is required';
}
return null;
},
onSaved: (String? value) {
_make = value;
},
);
}
Widget _buildVehicleModel() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Model'),
validator: (String? value) {
if (value == null) {
return 'Model is required';
}
return null;
},
onSaved: (String? value) {
_model = value;
},
);
}
Widget _buildVehicleYear() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Year'),
validator: (String? value) {
if (value == null) {
return 'Year is required';
}
return null;
},
onSaved: (String? value) {
_year = value;
},
);
}
Widget _buildVehicleColor() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Color'),
validator: (String? value) {
if (value == null) {
return 'Color is required';
}
return null;
},
onSaved: (String? value) {
_color = value;
},
);
}
Widget _buildVehicleVinNumber() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Vin Number'),
validator: (String? value) {
if (value == null) {
return 'Vin Number is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildVehiclePlate() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Plate Number'),
validator: (String? value) {
if (value == null) {
return 'Plate Number is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildOwnerName() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Owner Name'),
validator: (String? value) {
if (value == null) {
return 'Owner Name is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildOwnerAddress() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Owner Address'),
validator: (String? value) {
if (value == null) {
return 'Owner Address is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildOwnerCity() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Owner City'),
validator: (String? value) {
if (value == null) {
return 'Owner City is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildOwnerZip() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Owner Zip'),
validator: (String? value) {
if (value == null) {
return 'Owner Zip is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildOwnerPhone() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Owner Phone'),
validator: (String? value) {
if (value == null) {
return 'Owner Phone is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildInsuranceCo() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Owner Insurance Company'),
validator: (String? value) {
if (value == null) {
return 'Owner Insurance Company is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
// ignore: unused_element
Widget _buildInsurancePhone() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Insurance Phone'),
validator: (String? value) {
if (value == null) {
return 'Insurance Phone is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildPolicyHolderName() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Policy Holder'),
validator: (String? value) {
if (value == null) {
return 'Policy Holder is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildPolicyNumber() {
return TextFormField(
decoration: const InputDecoration(labelText: 'Policy Number'),
validator: (String? value) {
if (value == null) {
return 'Policy Number is required';
}
return null;
},
onSaved: (String? value) {
_vimNumber = value;
},
);
}
Widget _buildVehiclesCard() {
return Container(
margin: const EdgeInsets.all(8.0),
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(color: Colors.blueGrey, blurRadius: 10, spreadRadius: -10)
],
),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
padding: const EdgeInsets.all(10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.directions_car, color: Colors.teal),
IconButton(
onPressed: () {
setState(() {
print('Trash presses');
});
},
icon: const Icon(Icons.delete),
color: Colors.red,
),
],
),
buildTypeVeihicle(),
_buildIsCommercial(),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Expanded(child: _buildVehicleMake()),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
child: _buildVehicleModel()))
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Expanded(child: _buildVehicleYear()),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
child: _buildVehicleColor()))
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Expanded(child: _buildVehicleVinNumber()),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8.0, 0, 0, 0),
child: _buildVehiclePlate(),
)),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 20, 0, 0),
child: DropdownStatesUs())),
]),
const SizedBox(
height: 8,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: const [
Icon(
Icons.person,
color: Colors.teal,
),
],
),
Row(
children: [
Expanded(child: _buildOwnerName()),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
child: _buildOwnerPhone(),
)),
],
),
_buildOwnerAddress(),
Row(
children: [
Expanded(child: _buildOwnerCity()),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 20, 0, 0),
child: DropdownStatesUs(),
)),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
child: _buildOwnerZip(),
)),
],
),
Row(
children: [
Expanded(child: _buildPolicyHolderName()),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
child: _buildPolicyNumber(),
)),
],
)
],
),
),
),
);
}
//
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.teal,
child: const Icon(Icons.add),
onPressed: () {
vehicles.add(_buildVehiclesCard());
// print(vehicles.length);
setState(() {});
},
),
body: Form(
child: ListView.builder(
primary: false,
itemCount: vehicles.length,
itemBuilder: (BuildContext context, int i) {
return vehicles[i];
},
),
),
);
}
}
Thanks for any help beforehand, any other advices or tips to make the code better, let me know
The current state changes behavior reflected on newly item because last item created before the radio-buttom or switch tile was changed. Once you switch the button and change the radioGroup value it will only get to the new created widget that will be trigger by floating action button.
Notice that List<Widget> vehicles = []; holds the created widgets. And while creating new widget you are using state variables like vehicleType and isCommercial. Once you click on 1st generated widget, these variables get new data based on your tap event. while the state is holding these variables, then again you click on fab to add item on vehicles to generate item with current state of vehicleType and isCommercial.
While every list-item will have its own state, it is better to create a new StatefulWidget for each item. Also, you can go for state-management like riverpod, bloc for future purpose.
A simplify version but not complete, you need to handle callback to get changes data or better start using riverpod or bloc for state management
enum VehicleType { pov, leased, pgti }
class VehiclesPage extends StatefulWidget {
const VehiclesPage({Key? key}) : super(key: key);
#override
State<VehiclesPage> createState() => _VehiclesPageState();
}
class _VehiclesPageState extends State<VehiclesPage> {
List<MyDataModel> vehicles = [];
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.teal,
child: const Icon(Icons.add),
onPressed: () {
vehicles.add(MyDataModel());
setState(() {});
},
),
body: Form(
child: ListView.builder(
primary: false,
itemCount: vehicles.length,
itemBuilder: (BuildContext context, int i) {
return ListItem(model: vehicles[i]);
},
),
),
);
}
}
class MyDataModel {
final VehicleType vehicleType;
final bool isCommercial;
MyDataModel({
this.vehicleType = VehicleType.pov,
this.isCommercial = false,
});
MyDataModel copyWith({
VehicleType? vehicleType,
bool? isCommercial,
}) {
return MyDataModel(
vehicleType: vehicleType ?? this.vehicleType,
isCommercial: isCommercial ?? this.isCommercial,
);
}
}
class ListItem extends StatefulWidget {
final MyDataModel model;
const ListItem({
Key? key,
required this.model,
}) : super(key: key);
#override
State<ListItem> createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
late MyDataModel model = widget.model;
Widget buildTypeVeihicle() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ListTile(
horizontalTitleGap: 0,
dense: true,
title: const Text('POV'),
leading: Radio(
value: VehicleType.pov,
groupValue: model.vehicleType,
onChanged: (VehicleType? value) {
setState(() {
model = model.copyWith(vehicleType: value);
});
}),
),
),
Expanded(
child: ListTile(
horizontalTitleGap: 0,
dense: true,
title: const Text('Leased'),
leading: Radio(
value: VehicleType.leased,
groupValue: model.vehicleType,
onChanged: (VehicleType? value) {
setState(() {
model = model.copyWith(vehicleType: value);
});
}),
),
),
Expanded(
child: ListTile(
horizontalTitleGap: 0,
dense: true,
title: const Text(
'PGTI',
softWrap: false,
),
leading: Radio(
value: VehicleType.pgti,
groupValue: model.vehicleType,
onChanged: (VehicleType? value) {
setState(() {
model = model.copyWith(vehicleType: value);
});
}),
),
),
],
);
}
Widget _buildIsCommercial() {
return SwitchListTile(
title: const Text('This is commercial vehicle?'),
value: model.isCommercial,
onChanged: (bool value) {
setState(() {
model = model.copyWith(isCommercial: value);
});
},
// secondary: const Icon(Icons.car_repair),
);
}
Widget _buildVehiclesCard() {
return Container(
margin: const EdgeInsets.all(8.0),
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(color: Colors.blueGrey, blurRadius: 10, spreadRadius: -10)
],
),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
padding: const EdgeInsets.all(10),
child: Column(
children: [
buildTypeVeihicle(),
_buildIsCommercial(),
],
),
),
),
);
}
#override
Widget build(BuildContext context) {
return _buildVehiclesCard();
}
}

Flutter TextFormField Cursor Reset To First position of letter after onChanged

I need some help regarding flutter textformfield This is my code for the textfield controller. The problem is when I type new word,the cursor position is moved automatically from right to left (reset)(before first letter inside box). How I can make the cursor work as usual at the end of current text. I have read few solutions from stack overflow but it still not working. Please help me. Thanks.
class BillingWidget extends StatelessWidget {
final int pageIndex;
final Function validateController;
final formKey = new GlobalKey<FormState>();
BillingWidget(this.billingDetails,this.pageIndex,this.validateController);
final BillingDetails billingDetails;
#override
Widget build(BuildContext context) {
return Form(
key: formKey,
onChanged: () {
if (formKey.currentState.validate()) {
validateController(pageIndex,false);
formKey.currentState.save();
final val = TextSelection.collapsed(offset: _textTEC.text.length);
_textTEC.selection = val;
}
else {
//prevent procced to next page if validation is not successful
validateController(pageIndex,true);
}
},
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 20,bottom: 0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"Maklumat Pembekal",
textAlign: TextAlign.left,
style: TextStyle(
decoration:TextDecoration.underline,
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.grey.shade700,
),
),
),
),
TextFormField(
controller: billingDetails.companyNameTxtCtrl,
maxLength: 30,
decoration: InputDecoration(labelText: "Nama Syarikat"),
validator: (String value) {
return value.isEmpty ? 'Nama Syarikat Diperlukan' : null;
},
onSaved: (String value) {
billingDetails.companyName = value;
billingDetails.companyNameTxtCtrl.text = billingDetails.companyName;
},
),
TextFormField(
controller: billingDetails.addressLine1TxtCtrl,
maxLength: 30,
decoration: InputDecoration(labelText: "Alamat Baris 1"),
validator: (String value) {
return value.isEmpty ? 'Alamat Baris tidak boleh kosong.' : null;
},
onSaved: (String value) {
billingDetails.addressLine1 = value;
billingDetails.addressLine1TxtCtrl.text = billingDetails.addressLine1;
},
),
TextFormField(
controller: billingDetails.addressLine2TxtCtrl,
maxLength: 30,
decoration: InputDecoration(labelText: "Alamat Baris 2"),
onSaved: (String value) {
billingDetails.addressLine2 = value;
billingDetails.addressLine2TxtCtrl.text = billingDetails.addressLine2;
},
),
TextFormField(
controller: billingDetails.addressLine3TxtCtrl,
maxLength: 30,
decoration: InputDecoration(labelText: "Alamat Baris 3"),
onSaved: (String value) {
billingDetails.addressLine3 = value;
billingDetails.addressLine3TxtCtrl.text = billingDetails.addressLine3;
},
),
],
),
);
}
yourController.text = yourString;
yourController.selection = TextSelection.fromPosition(TextPosition(offset: yourController.text.length));

Passing onChanged Function to Radio results in 'Closure call with mismatched arguments'

I have the following build method in my stateful widget:
#override
Widget build(BuildContext context) {
return Column(children: [
Padding(
child: Container(
child: Row(
children: <Widget>[
_myRadioButton(
title: genders[0],
value: genders[0],
onChanged: (newValue) =>
setState(() => {_groupValue = newValue, context.read<UserSignupForm>().gender = newValue}),
),
_myRadioButton(
title: genders[1],
value: genders[1],
onChanged: (newValue) =>
setState(() => {
_groupValue = newValue, context.read<UserSignupForm>().gender = newValue}),
),
],
),
))
]);
}
And this is my Radio row:
Row _myRadioButton({required String title, String? value, required Function onChanged}) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Radio(
value: value,
groupValue: _groupValue,
onChanged: onChanged(),
),
Text(title)
],
);
}
However, I get the following runtime error when building the widget:
The following NoSuchMethodError was thrown building GenderField(dirty, state: _GenderFieldState#17448):
Closure call with mismatched arguments: function '_GenderFieldState.build.<anonymous closure>'
Receiver: Closure: (dynamic) => void
Tried calling: _GenderFieldState.build.<anonymous closure>()
Found: _GenderFieldState.build.<anonymous closure>(dynamic) => void
Any ideas how to correctly pass the onChanged method argument to the onChanged property?
Here is a solution with no compilation/runtime errors:
Row _myRadioButton({required String title, String? value,
required Function(dynamic)? onChanged}) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Radio(
value: value,
groupValue: _groupValue,
onChanged: onChanged,
),
Text(title)
],
);
}
remove the parentheses when passing the function
Row _myRadioButton({required String title, String? value, required void Function(String?) onChanged}) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Radio(
value: value,
groupValue: _groupValue,
onChanged: onChanged,
),
Text(title)
],
);
}

Flutter provider, Right way to use GlobalKey<FormState> in Provider

I'm new at Provider package. and Just making demo app for learning purpose.
Here is my code of simple Form Widget.
1) RegistrationPage (Where my app is start)
class RegistrationPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("Title"),
),
body: MultiProvider(providers: [
ChangeNotifierProvider<UserProfileProvider>.value(value: UserProfileProvider()),
ChangeNotifierProvider<RegiFormProvider>.value(value: RegiFormProvider()),
], child: AllRegistrationWidgets()),
);
}
}
class AllRegistrationWidgets extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SetProfilePicWidget(),
RegistrationForm(),
],
),
),
),
BottomSaveButtonWidget()
],
),
),
);
}
}
class BottomSaveButtonWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final _userPicProvider =
Provider.of<UserProfileProvider>(context, listen: false);
final _formProvider =
Provider.of<RegiFormProvider>(context, listen: false);
return SafeArea(
bottom: true,
child: Container(
margin: EdgeInsets.all(15),
child: FloatingActionButton.extended(
heroTag: 'saveform',
icon: null,
label: Text('SUBMIT',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
)),
onPressed: () {
print(_userPicProvider.strImageFileName);
_formProvider.globalFormKey.currentState.validate();
print(_formProvider.firstName);
print(_formProvider.lastName);
},
)),
);
}
}
2) RegistrationForm
class RegistrationForm extends StatefulWidget {
#override
_RegistrationFormState createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
TextEditingController _editingControllerFname;
TextEditingController _editingControllerLname;
#override
void initState() {
_editingControllerFname = TextEditingController();
_editingControllerLname = TextEditingController();
super.initState();
}
#override
Widget build(BuildContext context) {
final formProvider = Provider.of<RegiFormProvider>(context);
return _setupOtherWidget(formProvider);
}
_setupOtherWidget(RegiFormProvider _formProvider) {
return Container(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 20),
Text(
'Fields with (*) are required.',
style: TextStyle(fontStyle: FontStyle.italic),
textAlign: TextAlign.left,
),
SizedBox(height: 20),
_formSetup(_formProvider)
],
),
);
}
_formSetup(RegiFormProvider _formProvider) {
return Form(
key: _formProvider.globalFormKey,
child: Container(
child: Column(
children: <Widget>[
TextFormField(
controller: _editingControllerFname,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
labelText: "First Name *",
hintText: "First Name *",
),
onSaved: (value) {},
validator: (String value) =>
_formProvider.validateFirstName(value)),
SizedBox(height: 15),
TextFormField(
controller: _editingControllerLname,
textCapitalization: TextCapitalization.sentences,
validator: (String value) =>
_formProvider.validateLastName(value),
onSaved: (value) {},
decoration: InputDecoration(
labelText: "Last Name *",
hintText: "Last Name *",
),
)
],
),
),
);
}
#override
void dispose() {
_editingControllerFname.dispose();
_editingControllerLname.dispose();
super.dispose();
}
}
3) RegiFormProvider
class RegiFormProvider with ChangeNotifier {
final GlobalKey<FormState> globalFormKey = GlobalKey<FormState>();
String _strFirstName;
String _strLasttName;
String get firstName => _strFirstName;
String get lastName => _strLasttName;
String validateFirstName(String value) {
if (value.trim().length == 0)
return 'Please enter first name';
else {
_strFirstName = value;
return null;
}
}
String validateLastName(String value) {
if (value.trim().length == 0)
return 'Please enter last name';
else {
_strLasttName = value;
return null;
}
}
}
Here you can see, RegiFormProvider is my first page where other is children widgets in widget tree. I'm using final GlobalKey<FormState> globalFormKey = GlobalKey<FormState>(); in the RegiFormProvider provider, Because I want to access this in the 1st RegistrationPage to check my firstName and lastName is valid or not.
I'm using a builder widget to get form level context like below , and then easily we can get the form instance by using that context. by this way we don't need global key anymore.
Form(
child: Builder(
builder: (ctx) {
return ListView(
padding: EdgeInsets.all(12),
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: "Title"),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
initialValue: formProduct.title,
validator: validateTitle,
onSaved: (value) {
formProduct.title = value;
},
),
TextFormField(
decoration: InputDecoration(labelText: "Price"),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
initialValue: formProduct.price == null
? ""
: formProduct.price.toString(),
keyboardType: TextInputType.number,
validator: validatePrice,
onSaved: (value) {
formProduct.price = double.parse(value);
},
),
TextFormField(
decoration: InputDecoration(labelText: "Description"),
textInputAction: TextInputAction.next,
initialValue: formProduct.description,
maxLines: 3,
validator: validateDescription,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
onSaved: (value) {
formProduct.description = value;
},
),
TextFormField(
decoration: InputDecoration(labelText: "Image Url"),
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => FocusScope.of(context).unfocus(),
initialValue: formProduct.imageUrl,
validator: validateImageUrl,
onSaved: (value) {
formProduct.imageUrl = value;
},
),
Padding(
padding: EdgeInsets.all(10),
child: FlatButton(
color: Colors.amberAccent,
onPressed: () {
if (Form.of(ctx).validate()) {
Form.of(ctx).save();
formProduct.id =
Random.secure().nextDouble().toString();
ProductsProvider provider =
Provider.of<ProductsProvider>(context,
listen: false);
editing
? provider.setProduct(formProduct)
: provider.addProduct(formProduct);
Router.back(context);
}
},
child: Text("Save"),
),
)
],
);
},
),
)
you can see the Form.of(ctx) gives us the current level form.

Checkbox form validation

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,
),