I am creating a custom form field in Flutter : a list of custom checkboxes. I know I can use this package but I want to have my own checkboxes. Anyway, so
my main screen is like this :
class ProofAbsencesScreen extends StatefulWidget {
const ProofAbsencesScreen({super.key});
@override
State<ProofAbsencesScreen> createState() => _ProofAbsencesScreenState();
}
class _ProofAbsencesScreenState extends State<ProofAbsencesScreen> {
Map<int, bool> absences = <int, bool>{};
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
final c = Get.put(ProofAbsencesController());
return Obx( () => Form(
key: _formKey,
child : SelectAbsencesField(
items: c.noJustifiedAbs,
validator: (Map<int, bool>? value) {
bool valid = false;
value?.forEach((int id, bool checked) { if (checked) valid = true; });
return valid ? null : 'select_absence_error'.tr;
},
onSaved: (value) {
absences = value ?? <int, bool>{};
},
),
),
);
}
}
this is the code for my grouped field :
class SelectAbsencesField extends FormField<Map<int, bool>> {
SelectAbsencesField({
super.key,
required List<Absence> items,
required FormFieldValidator<Map<int, bool>> super.validator,
required super.onSaved,
}) :
super(
autovalidateMode: AutovalidateMode.onUserInteraction,
builder: (state) {
final defaultWidget = Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: state.hasError ? BorderSide(color: Colors.red.shade400,
width: 2,
) : BorderSide(color: Get.isDarkMode ? Colors.grey.shade800 : Colors.grey.shade200,),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Column(
children: items.map((e) =>
AbsenceCardCheckbox(
intitule: e.intitule ?? 'unknown'.tr,
date: e.date,
heureDebut: e.heureDebut,
onTapAction: (bool checked) {
final Map<int, bool> map = state.value ?? <int, bool>{};
map.update(e.id, (checked2) => checked, ifAbsent: () => true);
state.didChange(map);
},
) as Widget,
).toList(),
),
),
);
if (state.hasError) {
return Column(
children: [
defaultWidget,
Container(
margin: const EdgeInsets.only(top: 5,),
child: Text(state.errorText!, style: TextStyle(color: Get.isDarkMode ? Colors.red.shade400 : Colors.red.shade600),),
),
],
);
} else {
return defaultWidget;
}
},
);
}
and finally the code for the absence card checked box widget :
class AbsenceCardCheckbox extends StatefulWidget {
const AbsenceCardCheckbox({
super.key,
required this.intitule,
required this.date,
required this.heureDebut,
required this.onTapAction,
});
final String intitule;
final String date;
final String heureDebut;
final void Function(bool checked) onTapAction;
@override
State<AbsenceCardCheckbox> createState() => _AbsenceCardCheckboxState(
intitule: intitule,
date: date,
heureDebut: heureDebut,
onTapAction: onTapAction,
);
}
class _AbsenceCardCheckboxState extends State<AbsenceCardCheckbox> {
_AbsenceCardCheckboxState({
required this.intitule,
required this.date,
required this.heureDebut,
required this.onTapAction,
});
final String intitule;
final String date;
final String heureDebut;
void Function(bool checked) onTapAction;
bool checked = false;
@override
Widget build(BuildContext context) {
final dateP = DateTime.parse(
'$date $heureDebut',
);
final datetimeStr = DateFormat('dd/MM/yyyy HH:mm').format(dateP);
final dark = Get.isDarkMode;
return Container(
margin: EdgeInsets.zero,
color: dark ? const Color(0x1E1E1E1E) : Theme.of(context).cardColor,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 13,
horizontal: 16,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
intitule,
style: TextStyle(
color: dark ? Colors.orange.shade400 : Colors.orange.shade600,
),
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
datetimeStr,
style: TextStyle(
color: Colors.grey.shade400,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: GestureDetector(
onTap: () {
setState(() {
checked = !checked;
print('state was rebuilt and box is $checked');
});
onTapAction(checked);
},
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
border: Border.all(width: 1.5, color: dark ? Colors.white : Colors.black),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: AnimatedContainer(
height: 20,
width: 20,
color: checked ? Get.isDarkMode ? Colors.purple.shade200 : Theme.of(context).colorScheme.secondary :
dark ? const Color(0x1E1E1E1E) : Theme.of(context).cardColor,
alignment: AlignmentDirectional.centerEnd,
curve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 300),
),
),
),
),
),
],
),
),
);
}
}
My problem here is that checkbox state is not updating correctly. I recorded a video, take a close look at 4sec.
The first tap doesn’t update the checkbox state, I have to tap a second time to get the widget rebuild.
One weird thing is that the log message is printed by flutter for the first tap and checked is actually set to true so setState
is clearly called but for some reason it doesn’t change the widget state.
A last thing to mention : I have no idea why this happens but if I change my onTap
to this :
onTap: () {
setState(() {
checked = !checked;
});
//onTapAction(checked);
},
the bug doesn’t happen anymore.