Nodes
Understanding nodes in Vyuh Node Flow
Nodes
Nodes are the fundamental building blocks of your flow graph. They represent entities in your visual programming interface, workflow, or diagram.
Node Structure
A Node consists of several key components:
class Node<T extends NodeData> {
final String id; // Unique identifier
final String type; // Node type for categorization
final Observable<Offset> position; // Position on canvas
final Size size; // Dimensions
final T data; // Your custom data
final List<Port> inputPorts; // Input connection points
final List<Port> outputPorts; // Output connection points
final Observable<int> zIndex; // Layer order
}Creating Nodes
Basic Node
final node = Node<MyNodeData>(
id: 'node-1',
type: 'process',
position: Offset(100, 100),
size: Size(200, 100),
data: MyNodeData(title: 'Process Step'),
inputPorts: [
Port(
id: 'input-1',
name: 'Input',
position: PortPosition.left,
type: PortType.target,
),
],
outputPorts: [
Port(
id: 'output-1',
name: 'Output',
position: PortPosition.right,
type: PortType.source,
),
],
);Node with Multiple Ports
final conditionalNode = Node<MyNodeData>(
id: 'condition-1',
type: 'condition',
position: Offset(300, 100),
size: Size(180, 120),
data: MyNodeData(title: 'If/Else'),
inputPorts: [
Port(
id: 'cond-input',
name: 'Input',
position: PortPosition.left,
type: PortType.target,
),
],
outputPorts: [
Port(
id: 'true-output',
name: 'True',
position: PortPosition.right,
type: PortType.source,
offset: Offset(0, -20),
),
Port(
id: 'false-output',
name: 'False',
position: PortPosition.right,
type: PortType.source,
offset: Offset(0, 20),
),
],
);Custom Node Data
Your node data must extend NodeData:
class ProcessNodeData extends NodeData {
final String title;
final String description;
final Map<String, dynamic> config;
ProcessNodeData({
required this.title,
this.description = '',
this.config = const {},
});
@override
Map<String, dynamic> toJson() => {
'title': title,
'description': description,
'config': config,
};
@override
void fromJson(Map<String, dynamic> json) {
// Reconstruct from JSON if needed
}
}Node Types
Use the type field to categorize nodes:
enum NodeType {
start,
process,
condition,
end,
}
// Create typed nodes
final startNode = Node<MyData>(
type: NodeType.start.name,
// ...
);
final processNode = Node<MyData>(
type: NodeType.process.name,
// ...
);Benefits:
- Different visual styles based on type
- Type-specific validation rules
- Easy filtering and querying
Node Positioning
Absolute Positioning
node.position.value = Offset(100, 200);Relative Positioning
// Move right by 50 pixels
final currentPos = node.position.value;
node.position.value = currentPos + Offset(50, 0);Center in Viewport
final viewport = controller.viewport;
final centerX = viewport.x + (viewport.width / 2) - (node.size.width / 2);
final centerY = viewport.y + (viewport.height / 2) - (node.size.height / 2);
node.position.value = Offset(centerX, centerY);Z-Index and Layering
Control which nodes appear on top:
// Bring node to front
node.zIndex.value = controller.maxZIndex + 1;
// Send to back
node.zIndex.value = controller.minZIndex - 1;Node Widget Rendering
Provide a custom widget builder:
NodeFlowEditor<MyData>(
controller: controller,
nodeBuilder: (context, node) {
switch (node.type) {
case 'start':
return StartNodeWidget(node: node);
case 'process':
return ProcessNodeWidget(node: node);
case 'condition':
return ConditionNodeWidget(node: node);
default:
return DefaultNodeWidget(node: node);
}
},
)Example Node Widget
class ProcessNodeWidget extends StatelessWidget {
final Node<ProcessNodeData> node;
const ProcessNodeWidget({required this.node});
@override
Widget build(BuildContext context) {
return Container(
width: node.size.width,
height: node.size.height,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.settings, size: 32, color: Colors.blue),
SizedBox(height: 8),
Text(
node.data.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
if (node.data.description.isNotEmpty)
Text(
node.data.description,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
),
);
}
}Node Selection
Single Selection
controller.selectNode('node-1');Multi-Selection
controller.selectNode('node-1', multiSelect: true);
controller.selectNode('node-2', multiSelect: true);Clear Selection
controller.clearSelection();Get Selected Nodes
final selectedIds = controller.selectedNodeIds;
final selectedNodes = selectedIds
.map((id) => controller.graph.getNode(id))
.whereType<Node<MyData>>()
.toList();Node Operations
Add Node
controller.addNode(node);Remove Node
controller.removeNode('node-1');Update Node
final node = controller.graph.getNode('node-1');
if (node != null) {
node.position.value = Offset(200, 200);
// Node data is mutable if needed
}Find Nodes
// Get all nodes
final allNodes = controller.graph.nodes.values.toList();
// Get nodes by type
final processNodes = allNodes
.where((n) => n.type == 'process')
.toList();
// Get nodes in viewport
final viewportNodes = controller.graph.getNodesInRect(
Rect.fromLTWH(
viewport.x,
viewport.y,
viewport.width,
viewport.height,
),
);Interactive Nodes
Make nodes respond to interactions:
class InteractiveNodeWidget extends StatelessWidget {
final Node<MyData> node;
final VoidCallback onTap;
const InteractiveNodeWidget({
required this.node,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
// Node UI
child: Text(node.data.title),
),
);
}
}
// Usage in node builder
nodeBuilder: (context, node) {
return InteractiveNodeWidget(
node: node,
onTap: () {
// Handle tap
showDialog(
context: context,
builder: (_) => NodePropertiesDialog(node: node),
);
},
);
}Best Practices
- Unique IDs: Always use unique, meaningful IDs
- Type Naming: Use consistent type naming convention
- Data Immutability: Consider making NodeData immutable
- Size Consistency: Keep similar node types at similar sizes
- Port Placement: Place ports logically for flow direction
- Z-Index: Use sparingly, only when needed
- Widget Performance: Keep node widgets lightweight
Common Patterns
Factory Pattern for Nodes
class NodeFactory {
static Node<MyData> createStartNode(Offset position) {
return Node<MyData>(
id: 'start-${DateTime.now().millisecondsSinceEpoch}',
type: 'start',
position: position,
size: Size(100, 60),
data: MyData(title: 'Start'),
outputPorts: [
Port(
id: 'start-out',
name: 'Output',
position: PortPosition.right,
type: PortType.source,
),
],
);
}
static Node<MyData> createProcessNode(Offset position, String title) {
return Node<MyData>(
id: 'process-${DateTime.now().millisecondsSinceEpoch}',
type: 'process',
position: position,
size: Size(150, 80),
data: MyData(title: title),
inputPorts: [/* ... */],
outputPorts: [/* ... */],
);
}
}Next Steps
- Learn about Ports
- Explore Connections
- See Node Examples