In this tutorial, we’ll explore how to create a hierarchical node structure in Flutter or Flutter Find Widget in Tree using widgets and manage interactions like scaling, dragging, and adding child nodes.
Let’s dive into the code and understand how everything works together.
Table of Contents
Overview of Flutter Find Widget in Tree
We’ll build a Flutter application that visualizes hierarchical nodes. Each node can have children nodes, which can be expanded or collapsed. Users can also add new child nodes dynamically.
Setting Up the App -Flutter Find Widget in Tree
First, we set up a basic Flutter application with a MaterialApp
and a Scaffold
that hosts our hierarchical nodes view.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Hierarchical Nodes',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HierarchicalView(),
);
}
}
HierarchicalView Widget -Flutter Find Widget in Tree
Our main view is HierarchicalView
, a stateful widget that manages scaling, dragging, and holds the root node of our hierarchy.
class HierarchicalView extends StatefulWidget {
const HierarchicalView({Key? key}) : super(key: key);
@override
_HierarchicalViewState createState() => _HierarchicalViewState();
}
class _HierarchicalViewState extends State<HierarchicalView> {
double _scale = 1.0;
double _previousScale = 1.0;
Offset _offset = Offset.zero;
Offset _normalizedOffset = Offset.zero;
final GlobalKey _parentKey = GlobalKey();
final Map<GlobalKey, Offset> _nodePositions = {};
final Map<NodeData, bool> _expandedNodes = {};
final NodeData _parentNode = NodeData(
key: GlobalKey(),
label: 'Parent Node',
children: [
NodeData(
key: GlobalKey(),
label: 'Child 1',
),
NodeData(
key: GlobalKey(),
label: 'Child 2',
),
],
);
@override
void initState() {
super.initState();
_initializeExpandedNodes(_parentNode);
}
void _initializeExpandedNodes(NodeData node) {
_expandedNodes[node] = true;
for (var child in node.children) {
_initializeExpandedNodes(child);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hierarchical Nodes'),
),
body: Column(
children: [
Expanded(
child: GestureDetector(
onScaleStart: (ScaleStartDetails details) {
_previousScale = _scale;
_normalizedOffset = (_offset - details.focalPoint) / _scale;
setState(() {});
},
onScaleUpdate: (ScaleUpdateDetails details) {
_scale = _previousScale * details.scale;
_offset = details.focalPoint + _normalizedOffset * _scale;
setState(() {});
},
onScaleEnd: (ScaleEndDetails details) {
_previousScale = 1.0;
},
child: SingleChildScrollView(
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade200, Colors.blue.shade900],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Transform(
transform: Matrix4.identity()
..translate(_offset.dx, _offset.dy)
..scale(_scale),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Node(
key: _parentKey,
data: _parentNode,
nodePositions: _nodePositions,
expandedNodes: _expandedNodes,
onAddChild: _addChild,
),
],
),
),
),
),
),
),
],
),
);
}
void _addChild(NodeData parentNode, String childLabel) {
setState(() {
final newNode = NodeData(
key: GlobalKey(),
label: childLabel,
);
parentNode.addChild(newNode);
_expandedNodes[newNode] = true; // Automatically expand new nodes
});
}
}
NodeData Class for Flutter Find Widget in Tree
Represents a node in our hierarchy. Each NodeData
has a key, label, and potentially children nodes.
class NodeData {
final GlobalKey key;
final String label;
final List<NodeData> children;
NodeData({
required this.key,
required this.label,
List<NodeData>? children,
}) : children = children ?? [];
void addChild(NodeData child) {
children.add(child);
}
}
Node Widget
A stateful widget representing a node in the UI. It manages its expansion state and position updates.
class Node extends StatefulWidget {
final NodeData data;
final Map<GlobalKey, Offset> nodePositions;
final Map<NodeData, bool> expandedNodes;
final void Function(NodeData parentNode, String childLabel) onAddChild;
const Node({
Key? key,
required this.data,
required this.nodePositions,
required this.expandedNodes,
required this.onAddChild,
}) : super(key: key);
@override
_NodeState createState() => _NodeState();
}
class _NodeState extends State<Node> {
bool _expanded = false;
late final GlobalKey _key;
@override
void didUpdateWidget(Node oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance!.addPostFrameCallback((_) {
_updatePosition();
});
}
@override
void initState() {
super.initState();
_key = widget.data.key;
WidgetsBinding.instance!.addPostFrameCallback((_) {
_updatePosition();
});
_expanded = widget.expandedNodes[widget.data]!;
}
void _updatePosition() {
final RenderBox? renderBox =
_key.currentContext?.findRenderObject() as RenderBox?;
if (renderBox != null) {
final Offset position = renderBox.localToGlobal(Offset.zero);
widget.nodePositions[_key] = position;
}
}
void _showAddChildDialog() {
final TextEditingController childNameController = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add Child Node'),
content: TextField(
controller: childNameController,
decoration: const InputDecoration(
labelText: 'Child Name',
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
if (childNameController.text.isNotEmpty) {
widget.onAddChild(
widget.data,
childNameController.text,
);
Navigator.of(context).pop();
}
},
child: const Text('Add'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() {
_expanded = !_expanded;
widget.expandedNodes[widget.data] = _expanded;
});
},
child: Container(
key: ObjectKey(UniqueKey()),
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 6,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.data.label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.person, color: Colors.white),
onPressed: _showAddChildDialog,
),
],
),
),
),
if (_expanded && widget.data.children.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: widget.data.children
.map((child) => Node(
key: child.key,
data: child,
nodePositions: widget.nodePositions,
expandedNodes: widget.expandedNodes,
onAddChild: widget.onAddChild,
))
.toList(),
),
],
);
}
}
Also Read:
Conclusion
In this tutorial, we’ve covered how to build a hierarchical node structure in Flutter using widgets like StatefulWidget
, GestureDetector
, and Transform
.
We managed scaling and dragging gestures, added dynamic child nodes, and visualized the hierarchy using CustomPaint
and CustomPainter
.
This setup allows for a flexible and interactive way to display and manage hierarchical data in your Flutter applications. You can expand on this foundation by adding animations, further customizations, or integrating with backend data sources for more dynamic content.
Visit GitHub for full code and demo output.