Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

How to implement TreeView with three-state CheckBoxes featuring select all and unselected all functionality

Ask Question

I want to achieve a tree hierarchy list view. I have tried a few references that I found on pub.dev:
links to packages: https://pub.dev/packages/parent_child_checkbox

https://pub.dev/packages/list_treeview

I have tested them, but they do not meet my requirements. I need a n-level sub-tree with checkbox and selection, as shown in the image below. Does anyone have any ideas on how to achieve this or can anyone please provide guidance? Thank you.

this.isSelected = false, this.children = const <TreeNode>[], }) : checkBoxState = isSelected ? CheckBoxState.selected : (children.any((element) => element.checkBoxState != CheckBoxState.unselected) ? CheckBoxState.partial : CheckBoxState.unselected); TreeNode copyWith({ String? title, bool? isSelected, List<TreeNode>? children, return TreeNode( title: title ?? this.title, isSelected: isSelected ?? this.isSelected, children: children ?? this.children,

Your data could be something like this:

final nodes = [
  TreeNode(
    title: "title.1",
    children: [
      TreeNode(
        title: "title.1.1",
      TreeNode(
        title: "title.1.2",
        children: [
          TreeNode(
            title: "title.1.2.1",
          TreeNode(
            title: "title.1.2.2",
      TreeNode(
        title: "title.1.3",
  TreeNode(
    title: "title.2",
  TreeNode(
    title: "title.3",
    children: [
      TreeNode(
        title: "title.3.1",
      TreeNode(
        title: "title.3.2",
  TreeNode(
    title: "title.4",

create an enum for checkBox state:

enum CheckBoxState {
  selected,
  unselected,
  partial,

create a TitleCheckBox widget that has three states and shows title:

class TitleCheckBox extends StatelessWidget {
  const TitleCheckBox({
    Key? key,
    required this.title,
    required this.checkBoxState,
    required this.onChanged,
    required this.level,
  }) : super(key: key);
  final String title;
  final CheckBoxState checkBoxState;
  final VoidCallback onChanged;
  final int level;
  @override
  Widget build(BuildContext context) {
    final themeData = Theme.of(context);
    const size = 24.0;
    const borderRadius = BorderRadius.all(Radius.circular(3.0));
    return Row(
      children: [
        SizedBox(
          width: level * 16.0,
        IconButton(
          onPressed: onChanged,
          // borderRadius: borderRadius,
          icon: Container(
            height: size,
            width: size,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              border: Border.all(
                color: checkBoxState == CheckBoxState.unselected
                    ? themeData.unselectedWidgetColor
                    : themeData.primaryColor,
                width: 2.0,
              borderRadius: borderRadius,
              color: checkBoxState == CheckBoxState.unselected
                  ? Colors.transparent
                  : themeData.primaryColor,
            child: AnimatedSwitcher(
              duration: const Duration(
                milliseconds: 260,
              child: checkBoxState == CheckBoxState.unselected
                  ? const SizedBox(
                      height: size,
                      width: size,
                  : FittedBox(
                      key: ValueKey(checkBoxState.name),
                      fit: BoxFit.scaleDown,
                      child: Center(
                        child: checkBoxState == CheckBoxState.partial
                            ? Container(
                                height: 1.8,
                                width: 12.0,
                                decoration: const BoxDecoration(
                                  color: Colors.white,
                                  borderRadius: borderRadius,
                            : const Icon(
                                Icons.check,
                                color: Colors.white,
        const SizedBox(
          width: 8.0,
        Text(title),

Now implement the recursive TreeView with the selection logic:

class TreeView extends StatefulWidget {
  const TreeView({
    Key? key,
    required this.nodes,
    this.level = 0,
    required this.onChanged,
  }) : super(key: key);
  final List<TreeNode> nodes;
  final int level;
  final void Function(List<TreeNode> newNodes) onChanged;
  @override
  State<TreeView> createState() => _TreeViewState();
class _TreeViewState extends State<TreeView> {
  late List<TreeNode> nodes;
  @override
  void initState() {
    super.initState();
    nodes = widget.nodes;
  TreeNode _unselectAllSubTree(TreeNode node) {
    final treeNode = node.copyWith(
      isSelected: false,
      children: node.children.isEmpty
          ? null
          : node.children.map((e) => _unselectAllSubTree(e)).toList(),
    return treeNode;
  TreeNode _selectAllSubTree(TreeNode node) {
    final treeNode = node.copyWith(
      isSelected: true,
      children: node.children.isEmpty
          ? null
          : node.children.map((e) => _selectAllSubTree(e)).toList(),
    return treeNode;
  @override
  Widget build(BuildContext context) {
    if (widget.nodes != nodes) {
      nodes = widget.nodes;
    return ListView.builder(
      itemCount: nodes.length,
      physics: widget.level != 0 ? const NeverScrollableScrollPhysics() : null,
      shrinkWrap: widget.level != 0,
      itemBuilder: (context, index) {
        return ExpansionTile(
          title: TitleCheckBox(
            onChanged: () {
              switch (nodes[index].checkBoxState) {
                case CheckBoxState.selected:
                  nodes[index] = _unselectAllSubTree(nodes[index]);
                  break;
                case CheckBoxState.unselected:
                  nodes[index] = _selectAllSubTree(nodes[index]);
                  break;
                case CheckBoxState.partial:
                  nodes[index] = _unselectAllSubTree(nodes[index]);
                  break;
              if (widget.level == 0) {
                setState(() {});
              widget.onChanged(nodes);
            title: nodes[index].title,
            checkBoxState: nodes[index].checkBoxState,
            level: widget.level,
          trailing:
              nodes[index].children.isEmpty ? const SizedBox.shrink() : null,
          children: [
            TreeView(
              nodes: nodes[index].children,
              level: widget.level + 1,
              onChanged: (newNodes) {
                bool areAllItemsSelected = !nodes[index]
                    .children
                    .any((element) => !element.isSelected);
                nodes[index] = nodes[index].copyWith(
                  isSelected: areAllItemsSelected,
                  children: newNodes,
                widget.onChanged(nodes);
                if (widget.level == 0) {
                  setState(() {});

All done! you can use your TreeView like this:

 TreeView(
        onChanged: (newNodes) {},
        nodes: nodes,

and this is the result:
Hi i saw your code but it is not as i am expecting there should be expanded feature mean open and close the main parent you can see there is right side arrow so there should be according open and close while click on parent node. – Akash Jun 26 at 4:52 Second there should be work checkmark mean when only single selected then it will be show check mark but its parent should be - sign of icon while all child selected then it should be change with check mark icon of parent – Akash Jun 26 at 4:53 third when i click all group check box then all should be checked along with child too.when again click to uncheck then all should be unchecked.. – Akash Jun 26 at 4:56 my code has expansionTile so the items are closed at first and when you click them they will be opened. but I know that checkboxes don't work as you expected. I'll implement that within the next few hours and I'll update the answer. – Mahdi Dahouei Jun 26 at 7:22

Maybe you can create your own renderObject, I try my best for make the indent widget look like your image provided. Keep in mind this is not Sliver widget therefore this can cost some perform an issue.

also I suggest you take a look this video if youe interest about renderObject in flutter. https://www.youtube.com/watch?v=HqXNGawzSbY&t=7458s

main.dart

import 'package:flutter/material.dart';
import 'indent_widget.dart';
void main() {
  runApp(const MyApp());
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
class _MyHomePageState extends State<MyHomePage> {
  Widget _buildColumn() {
    return Row(
      children: [
        Checkbox(value: false, onChanged: (value) {}),
        const Expanded(child: Text('text'))
  @override
  Widget build(BuildContext context) {
    double tabSpace = 30;
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: IndentWidget(children: [
            _buildColumn(),
            IndentTab(
                tabSpace: tabSpace,
                child: IndentWidget(
                  children: [
                    IndentTab(
                        tabSpace: tabSpace,
                        child: IndentWidget(
                          children: [
                            _buildColumn(),
                            _buildColumn(),
                            IndentTab(
                                tabSpace: tabSpace,
                                child: IndentWidget(
                                  children: [
                                    _buildColumn(),
                                    _buildColumn(),
                                    IndentTab(
                                        tabSpace: tabSpace,
                                        child: IndentWidget(
                                          children: [
                                            _buildColumn(),
                                            _buildColumn(),
                                            _buildColumn(),
                                    _buildColumn(),
                                    IndentTab(
                                        tabSpace: tabSpace,
                                        child: IndentWidget(
                                          children: [
                                            _buildColumn(),
                                            _buildColumn(),
                                            _buildColumn(),
                            _buildColumn(),
                    _buildColumn(),
                    _buildColumn(),
                    _buildColumn(),
            _buildColumn(),

indent_widget.dart

import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
class IndentWidget extends MultiChildRenderObjectWidget {
  IndentWidget({super.key, super.children});
  @override
  RenderObject createRenderObject(BuildContext context) {
    /// 1. entry point.
    return RenderIndent();
/// provide information to RenderIndent;
class RenderIndentParentData extends ContainerBoxParentData<RenderBox> {
  double? tabSpace;
class IndentTab extends ParentDataWidget<RenderIndentParentData> {
  final double tabSpace;
  const IndentTab({super.key, required this.tabSpace, required super.child});
  @override
  void applyParentData(RenderObject renderObject) {
    final RenderIndentParentData parentData =
        renderObject.parentData! as RenderIndentParentData;
    if (parentData.tabSpace != tabSpace) {
      parentData.tabSpace = tabSpace;
      final targetObject = renderObject.parent;
      if (targetObject is RenderObject) {
        targetObject.markNeedsLayout();
  @override
  Type get debugTypicalAncestorWidgetClass => RenderIndentParentData;
class RenderIndent extends RenderBox
        ContainerRenderObjectMixin<RenderBox, RenderIndentParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, RenderIndentParentData> {
  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! RenderIndentParentData) {
      child.parentData = RenderIndentParentData();
  Size _performLayout(BoxConstraints constraints, bool dry) {
    RenderBox? child = firstChild;
    double width = 0, height = 0;
    while (child != null) {
      final RenderIndentParentData childParentData =
          child.parentData as RenderIndentParentData;
      final double leftShift = childParentData.tabSpace ?? 0;
      if (!dry) {
        childParentData.offset = Offset(leftShift, height);
        child.layout(BoxConstraints(maxWidth: constraints.maxWidth),
            parentUsesSize: true);
      height += child.size.height;
      width = max(width, leftShift + child.size.width);
      child = childParentData.nextSibling;
    if (width > constraints.maxWidth) {
      width = constraints.maxWidth;
    return Size(width, height);
  @override
  void performLayout() {
    size = _performLayout(constraints, false);
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _performLayout(constraints, true);
  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return defaultHitTestChildren(result, position: position);
        

Thanks for contributing an answer to Stack Overflow!

  • Please be sure to answer the question. Provide details and share your research!

But avoid

  • Asking for help, clarification, or responding to other answers.
  • Making statements based on opinion; back them up with references or personal experience.

To learn more, see our tips on writing great answers.