tree_view_view 1.0.2
tree_view_view: ^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_view.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),
],
),
),
),
),
),
);
},
);
},
),
),
],
);
}
}