Flutter's Checkbox widget is fully controlled — it has no internal state of its own, which means every value change must be explicitly reflected back through the parent widget's own state or the checkbox will appear stuck.
A basic checkbox
bool _isChecked = false;
Checkbox(
value: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value ?? false;
});
},
)
Without setState() inside onChanged, the checkbox visually reverts to its previous state on the next rebuild — since Checkbox is purely controlled by its value parameter, it has no memory of a click that wasn't also reflected in the parent's own state.
A checkbox with a label, using CheckboxListTile
CheckboxListTile(
title: const Text('Subscribe to newsletter'),
value: _isSubscribed,
onChanged: (value) {
setState(() {
_isSubscribed = value ?? false;
});
},
)
CheckboxListTile combines a checkbox with a tappable label and proper spacing in one widget — genuinely more convenient than manually placing a Checkbox and Text side by side in a Row, and it also makes the entire row (not just the small checkbox itself) tappable.
A tristate checkbox (checked, unchecked, or indeterminate)
bool? _selectAll = false;
Checkbox(
tristate: true,
value: _selectAll,
onChanged: (value) {
setState(() {
_selectAll = value;
});
},
)
tristate: true allows a third, indeterminate visual state (typically a dash rather than a check or empty box) — commonly used for a "select all" checkbox that should show this indeterminate state when only some, not all, items in a list are individually selected.
A list of checkboxes with a working "select all"
List _selected = List.filled(items.length, false);
bool? get _allSelected {
if (_selected.every((s) => s)) return true;
if (_selected.every((s) => !s)) return false;
return null; // some selected — indeterminate
}
void _toggleAll(bool? value) {
setState(() {
_selected = List.filled(items.length, value ?? false);
});
}
Computing the "select all" checkbox's own value from the individual items' state (rather than tracking it separately) keeps the two genuinely in sync — a manually tracked separate boolean can drift out of sync if items are checked or unchecked individually.
Customizing a checkbox's color and shape
Checkbox(
value: _isChecked,
onChanged: (value) => setState(() => _isChecked = value ?? false),
activeColor: Colors.deepPurple,
checkColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
)