Flutter: Parent and child checkboxes — Part 1
What we’ll cover in this tutorial series:
In this two-part series, we’ll —
- understand the
Checkbox
widget behavior. - explore
CheckboxListTile
widget, understand its limitations, and overcome them. - and finally, implement the Parent and child checkboxes (Part 2).
Prerequisites:
This is an intermediate level tutorial. It is assumed that the reader knows the basics of Flutter programming and has a basic idea about the working of the Checkbox
in Flutter.
If you are new to Flutter, get started from here.
If you are here to learn about the basics of Flutter’s checkbox, you can refer to these articles —
-> Official Checkbox documentation
-> A Medium article
Checkboxes are the most commonly used UI component in any platform. It allows the user to select one or more items from a set. It can also be used to turn an option on or off.
Understanding the Checkbox behavior
In Flutter, by default, Checkbox
displays two values — true
(checked/selected state), false
(unchecked/unselected state).
bool _value = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Checkbox(
value: _value,
onChanged: (val) {
setState(() {
_value = val;
});
},
),
),
);
}
If the tristate
property is enabled, Checkbox
displays three values —true
(selected), false
(unselected) and null
(intermediate).
bool _value = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Checkbox(
tristate: true,
value: _value,
onChanged: (val) {
setState(() {
_value = val;
});
},
),
),
);
}
From the documentation,
The checkbox can optionally display three values — true, false, and null — if tristate is true. When value is null a dash is displayed. By default tristate is false and the checkbox’s value must be true or false.
Summary:
By default, tristate
property is false
and Checkbox
displays two only values — true
and false
.
If thetristate
property is true
, Checkbox
displays three values — true
, false
and null
.
- true
-> checked/selected state
- false
-> unchecked/unselected state
- null
-> dash/intermediate state
CheckboxListTile widget, limitations and overcoming them
CheckboxListTile widget
In Flutter, Checkbox
doesn’t have an inbuilt label
property. If we want to show a label next to Checkbox
we can utilize the built-in CheckboxListTile
widget. CheckboxListTile
provides two ways of defining Checkbox
next to a label with its controlAffinity
property.
1. Leading checkbox: Checkbox and next to it is a label.controlAffinity: ListTileControlAffinity.leading
bool _isPickles = false;
bool _isTomato = false;
bool _isLettuce = false;Widget _buildBody(ThemeData themeData) {
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Leading Checkbox',
style: themeData.textTheme.title,
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 16, 12),
child: Text(
'Extras',
style: themeData.textTheme.body2.copyWith(
color: Colors.black87,
),
),
),
CheckboxListTile(
activeColor: Colors.indigo,
title: Text('Pickles'),
value: _isPickles,
onChanged: (value) {
setState(() {
_isPickles = value;
});
},
controlAffinity: ListTileControlAffinity.leading,
),
CheckboxListTile(
activeColor: Colors.indigo,
title: Text('Tomato'),
value: _isTomato,
onChanged: (value) {
setState(() {
_isTomato = value;
});
},
controlAffinity: ListTileControlAffinity.leading,
),
CheckboxListTile(
activeColor: Colors.indigo,
title: Text('Lettuce'),
value: _isLettuce,
onChanged: (value) {
setState(() {
_isLettuce = value;
});
},
controlAffinity: ListTileControlAffinity.leading,
),
],
);
2. Trailing checkbox: Label first and in the end a checkbox.controlAffinity: ListTileControlAffinity.trailing
bool _isMicrophone = false;
bool _isLocation = false;
bool _isHaptics = false;Widget _buildBody(ThemeData themeData) {
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Trailing Checkbox',
style: themeData.textTheme.title,
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Text(
'Access',
style: themeData.textTheme.body2.copyWith(
color: Colors.black87,
),
),
),
CheckboxListTile(
activeColor: Colors.indigo,
title: Text('Microphone access'),
value: _isMicrophone,
onChanged: (value) {
setState(() {
_isMicrophone = value;
});
},
controlAffinity: ListTileControlAffinity.trailing,
),
Divider(
indent: 16,
),
CheckboxListTile(
activeColor: Colors.indigo,
title: Text('Location access'),
value: _isLocation,
onChanged: (value) {
setState(() {
_isLocation = value;
});
},
controlAffinity: ListTileControlAffinity.trailing,
),
Divider(
indent: 16,
),
CheckboxListTile(
activeColor: Colors.indigo,
title: Text('Haptics'),
value: _isHaptics,
onChanged: (value) {
setState(() {
_isHaptics = value;
});
},
controlAffinity: ListTileControlAffinity.trailing,
),
],
);
}
CheckboxListTile limitations
For simple tasks like selecting items from a list or turning an option on/off CheckboxListTile
is more than enough. But it has certain limitations when it comes to parent and child checkboxes, like —
CheckboxListTile
doesn’t supporttristate
property.- According to
Checkbox
’s material design spec, parent and children checkboxes have different indentations to signify the difference between them.CheckboxListTile
doesn’t support this as well.
Overcoming the limitations
We’ll overcome these limitations by creating our very own CustomLabeledCheckbox
that looks and behaves differently for parent and children checkboxes. It looks something like this,
Let’s create CustomLabeledCheckbox
—
- Create a new dart file named custom_labeled_checkbox.dart.
- First, let's create an enum,
CheckboxType
to differentiate between parent and children checkboxes.
enum CheckboxType {
Parent,
Child,
}
- Create a
CustomLabeledCheckbox
stateless widget with the following properties:
class CustomLabeledCheckbox extends StatelessWidget {
const CustomLabeledCheckbox({
@required this.label,
@required this.value,
@required this.onChanged,
this.checkboxType: CheckboxType.Child,
this.activeColor,
}) : assert(label != null),
assert(checkboxType != null),
assert(
(checkboxType == CheckboxType.Child && value != null) ||
(checkboxType == CheckboxType.Parent &&
(value != null || value == null)),
),
tristate = checkboxType == CheckboxType.Parent ? true : false;
final String label;
final bool value;
final bool tristate;
final ValueChanged<bool> onChanged;
final CheckboxType checkboxType;
final Color activeColor;
}
label
,value
, andonChanged
properties are mandatory. DefaultcheckboxType is CheckboxType.Child
and depending upon the value ofcheckboxType
,tristate
value is implicitly assigned in the constructor to avoid any mistakes.- If
checkboxType == CheckboxType.Parent
, then —tristate = true
and the possible return values arefalse
,true
, andnull
.
IfcheckboxType == CheckboxType.Child
, then —tristate = false
and the possible return values arefalse
, andtrue
. - If you need extra customization like
hoverColor
property and more, add them as properties and request the same in the constructor. - Now let's build the widget —
@override
Widget build(BuildContext context) {
final themeData = Theme.of(context);
return InkWell(
onTap: _onChanged,
child: Padding(
padding: EdgeInsets.only(left: 10, right: 16),
child: Row(
children: <Widget>[
checkboxType == CheckboxType.Child
? SizedBox(width: 32)
: SizedBox(width: 0),
Checkbox(
tristate: tristate,
value: value,
onChanged: (val) {
_onChanged();
},
activeColor: activeColor ?? themeData.toggleableActiveColor,
),
SizedBox(width: 8),
Text(
label,
style: themeData.textTheme.subhead,
)
],
),
),
);
}void _onChanged() {
if (value != null) {
onChanged(!value);
} else {
onChanged(value);
}
}
- We are using the
InkWell
widget to make the whole row interactive. InkWell provides both interaction and material ripple effect.onTap:
calls_onChanged()
method. Checkbox
and label are placed next to each other using aRow
widget with apadding
specified in the material design spec.- If the
checkboxType
isCheckboxType.Parent
left offset is 0 and if it isCheckboxType.Child
then the left offset is 32. Offset is provided by using theSizedBox
widget. - Now build the
Checkbox
with the properties obtained from the constructor.onChanged:
calls the_onChanged()
method.
Note: Here we are not utilizing theval
property that is available in theonChanged
callback of theCheckbox
. - Then define the label with 8 point spacing from the
Checkbox
withtextStyle = subhead
. That’s all for the UI. - Now understanding the
_onChanged()
method is the most important thing here. The_onChanged()
method triggers theonChanged
callback received through the constructor and sends back the relevant value.
-> ForCheckboxType.Child
—
* if the current value istrue
then the returned value through callback isfalse
i.e, selected to unselected state transition.
* if the current value isfalse
then the returned value through callback istrue
i.e, unselected to selected state transition.
-> ForCheckboxType.Parent
—
* if the current value istrue
then the returned value through callback isfalse
i.e, selected to unselected state transition.
* if the current value isfalse
then the returned value through callback istrue
i.e, unselected to selected state transition.
* if the current value isnull
then the returned value through callback is alsonull
(!important!) i.e, the intermediate state remains as it is and thenull
value is handled later to achieve the expected behavior (this will be explained thoroughly in Part — 2).
You now have a CustomLabeledCheckbox
that is capable of handling the tristate
and ready to be used with parent and child checkboxes. Implementation of parent and child checkboxes and it’s working will be explained in the Part — 2 of the series.