Flutter: Parent and child checkboxes — Part 1

Nishanth Sreedhara
7 min readApr 30, 2020

--

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.

Using checkboxes to select multiple items in a list
Using checkbox 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).

Selected and unselected state transition
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).

Unselected, selected and intermediate state transition
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

Suitable when you have to select items from the list
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

Suitable when used to turn off and on an option
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 —

  1. CheckboxListTile doesn’t support tristate property.
  2. 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,

One parent and four children CustomLabeledCheckboxes

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, and onChanged properties are mandatory. Default checkboxType is CheckboxType.Child and depending upon the value of checkboxType, tristate value is implicitly assigned in the constructor to avoid any mistakes.
  • If checkboxType == CheckboxType.Parent, then — tristate = true and the possible return values are false, true, and null.
    If checkboxType == CheckboxType.Child, then — tristate = false and the possible return values are false, and true.
  • 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 a Row widget with apadding specified in the material design spec.
  • If the checkboxType is CheckboxType.Parent left offset is 0 and if it is CheckboxType.Child then the left offset is 32. Offset is provided by using the SizedBox widget.
  • Now build the Checkbox with the properties obtained from the constructor.
    onChanged: calls the _onChanged() method.
    Note:
    Here we are not utilizing the val property that is available in the onChanged callback of the Checkbox.
  • Then define the label with 8 point spacing from the Checkbox with textStyle = subhead. That’s all for the UI.
  • Now understanding the _onChanged() method is the most important thing here. The _onChanged() method triggers the onChanged callback received through the constructor and sends back the relevant value.
    -> For CheckboxType.Child
    * if the current value is true then the returned value through callback is false i.e, selected to unselected state transition.
    *
    if the current value is false then the returned value through callback is true i.e, unselected to selected state transition.
    -> For CheckboxType.Parent
    * if the current value is true then the returned value through callback is false i.e, selected to unselected state transition.
    *
    if the current value is false then the returned value through callback is true i.e, unselected to selected state transition.
    * if the current value is null then the returned value through callback is also null (!important!) i.e, the intermediate state remains as it is and the null 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.

Complete code of CustomLabeledCheckbox on Github —

custom_labeled_checkbox.dart

--

--