tree_view_package 1.0.2 copy "tree_view_package: ^1.0.2" to clipboard
tree_view_package: ^1.0.2 copied to clipboard

A powerful and flexible tree view package for Flutter with drag-and-drop, animations, and customizable indentation guides.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:tree_view_package/tree_view_package.dart';

import 'tree_dropdown_example.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Tree View Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ExampleHomePage(),
    );
  }
}

class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({super.key});

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Tree View Examples'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Row(
        children: [
          NavigationRail(
            selectedIndex: _selectedIndex,
            onDestinationSelected: (int index) {
              setState(() {
                _selectedIndex = index;
              });
            },
            labelType: NavigationRailLabelType.all,
            destinations: const [
              NavigationRailDestination(
                icon: Icon(Icons.account_tree_outlined),
                selectedIcon: Icon(Icons.account_tree),
                label: Text('Basic'),
              ),
              NavigationRailDestination(
                icon: Icon(Icons.animation_outlined),
                selectedIcon: Icon(Icons.animation),
                label: Text('Animated'),
              ),
              NavigationRailDestination(
                icon: Icon(Icons.style_outlined),
                selectedIcon: Icon(Icons.style),
                label: Text('Guides'),
              ),
              NavigationRailDestination(
                icon: Icon(Icons.check_box_outlined),
                selectedIcon: Icon(Icons.check_box),
                label: Text('Checkbox'),
              ),
              NavigationRailDestination(
                icon: Icon(Icons.arrow_drop_down_outlined),
                selectedIcon: Icon(Icons.arrow_drop_down),
                label: Text('Dropdown'),
              ),
              NavigationRailDestination(
                icon: Icon(Icons.open_with_outlined),
                selectedIcon: Icon(Icons.open_with),
                label: Text('Drag & Drop'),
              ),
            ],
          ),
          const VerticalDivider(thickness: 1, width: 1),
          Expanded(
            child: IndexedStack(
              index: _selectedIndex,
              children: const [
                BasicTreeExample(),
                AnimatedTreeExample(),
                IndentGuidesExample(),
                CheckboxSelectExample(),
                TreeDropdownExample(),
                DragAndDropExample(),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ============================================================================
// Basic Tree Example
// ============================================================================

class Node {
  Node({required this.title, this.children = const []});

  final String title;
  final List<Node> children;
}

class BasicTreeExample extends StatefulWidget {
  const BasicTreeExample({super.key});

  @override
  State<BasicTreeExample> createState() => _BasicTreeExampleState();
}

class _BasicTreeExampleState extends State<BasicTreeExample> {
  late final TreeController<Node> treeController;

  static final List<Node> sampleData = [
    Node(
      title: 'Documents',
      children: [
        Node(
          title: 'Work',
          children: [
            Node(title: 'Project A'),
            Node(title: 'Project B'),
            Node(title: 'Project C'),
          ],
        ),
        Node(
          title: 'Personal',
          children: [
            Node(title: 'Photos'),
            Node(title: 'Videos'),
          ],
        ),
      ],
    ),
    Node(
      title: 'Downloads',
      children: [
        Node(title: 'file1.pdf'),
        Node(title: 'file2.pdf'),
      ],
    ),
    Node(title: 'Desktop'),
  ];

  @override
  void initState() {
    super.initState();
    treeController = TreeController<Node>(
      roots: sampleData,
      childrenProvider: (node) => node.children,
    );
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Row(
            children: [
              ElevatedButton.icon(
                onPressed: () => treeController.expandAll(),
                icon: const Icon(Icons.unfold_more),
                label: const Text('Expand All'),
              ),
              const SizedBox(width: 8),
              ElevatedButton.icon(
                onPressed: () => treeController.collapseAll(),
                icon: const Icon(Icons.unfold_less),
                label: const Text('Collapse All'),
              ),
            ],
          ),
        ),
        const Divider(),
        Expanded(
          child: TreeView<Node>(
            treeController: treeController,
            nodeBuilder: (context, entry) {
              return InkWell(
                onTap: () => treeController.toggleExpansion(entry.node),
                child: TreeIndentation(
                  entry: entry,
                  guide: const IndentGuide(indent: 40),
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Row(
                      children: [
                        FolderButton(
                          isOpen: entry.hasChildren ? entry.isExpanded : null,
                          onPressed: entry.hasChildren
                              ? () => treeController.toggleExpansion(entry.node)
                              : null,
                        ),
                        const SizedBox(width: 8),
                        Expanded(
                          child: Text(
                            entry.node.title,
                            style: const TextStyle(fontSize: 16),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

// ============================================================================
// Animated Tree Example
// ============================================================================

class AnimatedTreeExample extends StatefulWidget {
  const AnimatedTreeExample({super.key});

  @override
  State<AnimatedTreeExample> createState() => _AnimatedTreeExampleState();
}

class _AnimatedTreeExampleState extends State<AnimatedTreeExample> {
  late final TreeController<Node> treeController;

  @override
  void initState() {
    super.initState();
    treeController = TreeController<Node>(
      roots: _BasicTreeExampleState.sampleData,
      childrenProvider: (node) => node.children,
    );
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Padding(
          padding: EdgeInsets.all(16.0),
          child: Text(
            'Tree with smooth expand/collapse animations',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
        ),
        const Divider(),
        Expanded(
          child: AnimatedTreeView<Node>(
            treeController: treeController,
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeInOut,
            nodeBuilder: (context, entry) {
              return Card(
                margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
                elevation: entry.level.toDouble(),
                child: InkWell(
                  onTap: () => treeController.toggleExpansion(entry.node),
                  child: TreeIndentation(
                    entry: entry,
                    child: Padding(
                      padding: const EdgeInsets.all(12.0),
                      child: Row(
                        children: [
                          Icon(
                            entry.hasChildren
                                ? (entry.isExpanded
                                    ? Icons.keyboard_arrow_down_rounded
                                    : Icons.keyboard_arrow_right_rounded)
                                : Icons.insert_drive_file,
                            color: Theme.of(context).colorScheme.primary,
                          ),
                          const SizedBox(width: 12),
                          Expanded(
                            child: Text(
                              entry.node.title,
                              style: const TextStyle(fontSize: 16),
                            ),
                          ),
                          if (entry.hasChildren)
                            Icon(
                              entry.isExpanded
                                  ? Icons.expand_less
                                  : Icons.expand_more,
                            ),
                        ],
                      ),
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

// ============================================================================
// Indent Guides Example
// ============================================================================

class IndentGuidesExample extends StatefulWidget {
  const IndentGuidesExample({super.key});

  @override
  State<IndentGuidesExample> createState() => _IndentGuidesExampleState();
}

class _IndentGuidesExampleState extends State<IndentGuidesExample> {
  late final TreeController<Node> treeController;
  int _selectedGuide = 0;

  @override
  void initState() {
    super.initState();
    treeController = TreeController<Node>(
      roots: _BasicTreeExampleState.sampleData,
      childrenProvider: (node) => node.children,
      defaultExpansionState: true,
      enableCheckbox: true,
    );
    treeController.addListener(() {
      setState(() {}); // Rebuild to show selected count
    });
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  IndentGuide get _currentGuide {
    switch (_selectedGuide) {
      case 0:
        return const IndentGuide(indent: 40);
      case 1:
        return const IndentGuide.connectingLines(
          indent: 30,
          color: Colors.grey,
          thickness: 0.5,
        );
      case 2:
        return const IndentGuide.connectingLines(
          indent: 30,
          color: Colors.blue,
          thickness: 0.5,
          roundCorners: true,
        );
      case 3:
        return const IndentGuide.scopingLines(
          indent: 30,
          color: Colors.green,
          thickness: 0.5,
        );
      default:
        return const IndentGuide(indent: 30);
    }
  }

  @override
  Widget build(BuildContext context) {
    final checkedNodes = treeController.getCheckedNodes();

    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              SegmentedButton<int>(
                segments: const [
                  ButtonSegment(value: 0, label: Text('None')),
                  ButtonSegment(value: 1, label: Text('Connecting')),
                  ButtonSegment(value: 2, label: Text('Rounded')),
                  ButtonSegment(value: 3, label: Text('Scoping')),
                ],
                selected: {_selectedGuide},
                onSelectionChanged: (Set<int> newSelection) {
                  setState(() {
                    _selectedGuide = newSelection.first;
                  });
                },
              ),
              const SizedBox(height: 16),
              Row(
                children: [
                  Expanded(
                    child: Text(
                      'Selected: ${checkedNodes.length} items',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  if (checkedNodes.isNotEmpty)
                    ElevatedButton.icon(
                      onPressed: () => treeController.clearCheckedNodes(),
                      icon: const Icon(Icons.clear),
                      label: const Text('Clear Selection'),
                    ),
                ],
              ),
              if (checkedNodes.isNotEmpty)
                Padding(
                  padding: const EdgeInsets.only(top: 8.0),
                  child: Wrap(
                    spacing: 8,
                    children: checkedNodes.map((node) {
                      return Chip(
                        label: Text(node.title),
                        onDeleted: () {
                          treeController.setCheckboxState(node, false);
                        },
                      );
                    }).toList(),
                  ),
                ),
            ],
          ),
        ),
        const Divider(),
        Expanded(
          child: TreeView<Node>(
            enableCheckbox: true,
            enableSelectAll: true,
            treeController: treeController,
            nodeBuilder: (context, entry) {
              final checkbox = TreeCheckboxScope.buildCheckbox(context);

              return InkWell(
                onTap: () => treeController.toggleExpansion(entry.node),
                child: TreeIndentation(
                  entry: entry,
                  guide: _currentGuide,
                  child: Padding(
                    padding: const EdgeInsets.symmetric(
                      vertical: 0,
                      horizontal: 0,
                    ),
                    child: Row(
                      children: [
                        if (checkbox != null) checkbox,
                        FolderButton(
                          padding: EdgeInsetsGeometry.zero,
                          openedIcon: const Icon(
                            Icons.keyboard_arrow_down_rounded,
                            weight: 1,
                            size: 18,
                          ),
                          closedIcon: const Icon(
                            Icons.keyboard_arrow_right_rounded,
                            weight: 1,
                            size: 18,
                          ),
                          isOpen: entry.hasChildren ? entry.isExpanded : null,
                          onPressed: entry.hasChildren
                              ? () => treeController.toggleExpansion(entry.node)
                              : null,
                        ),
                        const SizedBox(width: 8),
                        Text(entry.node.title),
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

// ============================================================================
// Checkbox Select All/Unselect All Example
// ============================================================================

class CheckboxSelectExample extends StatefulWidget {
  const CheckboxSelectExample({super.key});

  @override
  State<CheckboxSelectExample> createState() => _CheckboxSelectExampleState();
}

class _CheckboxSelectExampleState extends State<CheckboxSelectExample> {
  late final TreeController<Node> treeController;

  static final List<Node> sampleData = [
    Node(
      title: 'Project',
      children: [
        Node(
          title: 'Frontend',
          children: [
            Node(title: 'React Components'),
            Node(title: 'Vue Components'),
            Node(title: 'Angular Modules'),
          ],
        ),
        Node(
          title: 'Backend',
          children: [
            Node(title: 'Node.js APIs'),
            Node(title: 'Python Services'),
            Node(title: 'Database Scripts'),
          ],
        ),
        Node(
          title: 'Mobile',
          children: [
            Node(title: 'iOS App'),
            Node(title: 'Android App'),
            Node(title: 'Flutter App'),
          ],
        ),
        Node(
          title: 'Documentation',
          children: [
            Node(title: 'User Guide'),
            Node(title: 'API Reference'),
            Node(title: 'Architecture'),
          ],
        ),
      ],
    ),
  ];

  @override
  void initState() {
    super.initState();
    treeController = TreeController<Node>(
      roots: sampleData,
      childrenProvider: (node) => node.children,
      defaultExpansionState: true,
      enableCheckbox: true,
    );
    treeController.addListener(() {
      setState(() {}); // Rebuild to show selected count
    });
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final checkedNodes = treeController.getCheckedNodes();

    return Column(
      children: [
        Container(
          padding: const EdgeInsets.all(16.0),
          color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Checkbox Selection with Select All / Unselect All',
                style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
              ),
              const SizedBox(height: 8),
              Text(
                'This example demonstrates the built-in Select All/Unselect All button that appears automatically when enableSelectAll is true. '
                'The button intelligently switches between "Select All" and "Unselect All" based on the current selection state.',
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              const SizedBox(height: 16),
              if (checkedNodes.isNotEmpty) ...[
                Container(
                  padding: const EdgeInsets.all(12),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.surface,
                    borderRadius: BorderRadius.circular(8),
                    border: Border.all(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Selected Items:',
                        style: Theme.of(context).textTheme.titleSmall,
                      ),
                      const SizedBox(height: 8),
                      Wrap(
                        spacing: 8,
                        runSpacing: 8,
                        children: checkedNodes.map((node) {
                          return Chip(
                            avatar: const Icon(Icons.check_circle, size: 18),
                            label: Text(node.title),
                            onDeleted: () {
                              treeController.setCheckboxState(node, false);
                            },
                            deleteIcon: const Icon(Icons.close, size: 18),
                          );
                        }).toList(),
                      ),
                    ],
                  ),
                ),
              ],
            ],
          ),
        ),
        const Divider(height: 1),
        Expanded(
          child: TreeView<Node>(
            enableCheckbox: true,
            enableSelectAll: true,
            treeController: treeController,
            nodeBuilder: (context, entry) {
              return InkWell(
                onTap: () => treeController.toggleCheckbox(entry.node),
                child: TreeIndentation(
                  entry: entry,
                  guide: const IndentGuide.connectingLines(
                    indent: 40,
                    color: Colors.grey,
                  ),
                  child: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 8.0),
                    child: Row(
                      children: [
                        FolderButton(
                          openedIcon: const Icon(Icons.folder_open, size: 20),
                          closedIcon: const Icon(Icons.folder, size: 20),
                          isOpen: entry.hasChildren ? entry.isExpanded : null,
                          onPressed: entry.hasChildren
                              ? () => treeController.toggleExpansion(entry.node)
                              : null,
                        ),
                        const SizedBox(width: 8),
                        if (!entry.hasChildren)
                          const Icon(Icons.insert_drive_file,
                              size: 18, color: Colors.grey),
                        if (!entry.hasChildren) const SizedBox(width: 8),
                        Expanded(
                          child: Text(
                            entry.node.title,
                            style: const TextStyle(fontSize: 14),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  int _countAllNodes(List<Node> nodes) {
    int count = 0;
    for (final node in nodes) {
      count++;
      count += _countAllNodes(node.children);
    }
    return count;
  }
}

// ============================================================================
// Drag and Drop Example
// ============================================================================

class DragNode {
  DragNode({required this.title, List<DragNode>? children, this.parent})
      : children = children ?? [];

  final String title;
  final List<DragNode> children;
  DragNode? parent;
}

class DragAndDropExample extends StatefulWidget {
  const DragAndDropExample({super.key});

  @override
  State<DragAndDropExample> createState() => _DragAndDropExampleState();
}

class _DragAndDropExampleState extends State<DragAndDropExample> {
  late final TreeController<DragNode> treeController;
  late List<DragNode> roots;

  @override
  void initState() {
    super.initState();
    roots = _createSampleData();
    treeController = TreeController<DragNode>(
      roots: roots,
      childrenProvider: (node) => node.children,
      parentProvider: (node) => node.parent,
      defaultExpansionState: true,
    );
  }

  List<DragNode> _createSampleData() {
    final root1 = DragNode(title: 'Folder 1');
    final child1 = DragNode(title: 'Item 1-1', parent: root1);
    final child2 = DragNode(title: 'Item 1-2', parent: root1);
    root1.children.addAll([child1, child2]);

    final root2 = DragNode(title: 'Folder 2');
    final child3 = DragNode(title: 'Item 2-1', parent: root2);
    root2.children.add(child3);

    return [root1, root2];
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  void _handleDrop(TreeDragAndDropDetails<DragNode> details) {
    final draggedNode = details.draggedNode;
    final targetNode = details.targetNode;

    // Remove from old parent
    draggedNode.parent?.children.remove(draggedNode);

    // Add to new parent
    if (!targetNode.children.contains(draggedNode)) {
      targetNode.children.add(draggedNode);
      draggedNode.parent = targetNode;
    }

    treeController.expand(targetNode);
    treeController.rebuild();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Padding(
          padding: EdgeInsets.all(16.0),
          child: Text(
            'Drag and drop nodes to reorganize the tree',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
        ),
        const Divider(),
        Expanded(
          child: TreeView<DragNode>(
            enableCheckbox: true,
            treeController: treeController,
            nodeBuilder: (context, entry) {
              return TreeDragTarget<DragNode>(
                node: entry.node,
                onNodeAccepted: _handleDrop,
                builder: (context, details) {
                  final isHovering = details != null;

                  return TreeDraggable<DragNode>(
                    node: entry.node,
                    feedback: Material(
                      elevation: 4,
                      child: Container(
                        padding: const EdgeInsets.all(8),
                        decoration: BoxDecoration(
                          color: Colors.blue.shade100,
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(entry.node.title),
                      ),
                    ),
                    child: Container(
                      decoration: BoxDecoration(
                        color: isHovering
                            ? Colors.blue.withOpacity(0.1)
                            : Colors.transparent,
                        border: isHovering
                            ? Border.all(color: Colors.blue, width: 2)
                            : null,
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: InkWell(
                        onTap: () => treeController.toggleExpansion(entry.node),
                        child: TreeIndentation(
                          entry: entry,
                          guide: const IndentGuide.connectingLines(indent: 40),
                          child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Row(
                              children: [
                                Icon(
                                  entry.hasChildren
                                      ? Icons.folder
                                      : Icons.insert_drive_file,
                                ),
                                const SizedBox(width: 8),
                                Expanded(child: Text(entry.node.title)),
                                const Icon(Icons.drag_indicator),
                              ],
                            ),
                          ),
                        ),
                      ),
                    ),
                  );
                },
              );
            },
          ),
        ),
      ],
    );
  }
}
2
likes
150
points
381
downloads

Publisher

unverified uploader

Weekly Downloads

A powerful and flexible tree view package for Flutter with drag-and-drop, animations, and customizable indentation guides.

Homepage
Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

flutter, shadcn_flutter

More

Packages that depend on tree_view_package