Controller
Programmatic control of your node flow graph
NodeFlowController
The NodeFlowController is the central API for managing and manipulating your node flow graph programmatically.
Creating a Controller
class _MyEditorState extends State<MyEditor> {
late final NodeFlowController<MyNodeData> controller;
@override
void initState() {
super.initState();
controller = NodeFlowController<MyNodeData>();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}Core Properties
Graph
Access the underlying graph model:
final graph = controller.graph;
// Get all nodes
final nodes = graph.nodes.values.toList();
// Get all connections
final connections = graph.connections.values.toList();
// Get specific node
final node = graph.getNode('node-1');
// Get specific connection
final connection = graph.getConnection('conn-1');Viewport
Control the visible area of the canvas:
final viewport = controller.viewport;
// Get current position
final x = viewport.x;
final y = viewport.y;
// Get dimensions
final width = viewport.width;
final height = viewport.height;
// Get zoom level
final zoom = viewport.zoom;Node Operations
Add Node
final node = Node<MyData>(
id: 'new-node',
type: 'process',
position: Offset(200, 100),
size: Size(150, 80),
data: MyData(label: 'New Node'),
inputPorts: [/* ... */],
outputPorts: [/* ... */],
);
controller.addNode(node);Remove Node
controller.removeNode('node-1');This automatically removes all connections to/from the node.
Update Node Position
final node = controller.graph.getNode('node-1');
if (node != null) {
node.position.value = Offset(300, 200);
}Get Node
final node = controller.graph.getNode('node-1');Get All Nodes
final allNodes = controller.graph.nodes.values.toList();Find Nodes in Area
final nodesInRect = controller.graph.getNodesInRect(
Rect.fromLTWH(0, 0, 500, 500),
);Connection Operations
Add Connection
final connection = Connection(
id: 'conn-1',
sourceNodeId: 'node-1',
sourcePortId: 'out-1',
targetNodeId: 'node-2',
targetPortId: 'in-1',
);
controller.addConnection(connection);Remove Connection
controller.removeConnection('conn-1');Get Connection
final connection = controller.graph.getConnection('conn-1');Get All Connections
final allConnections = controller.graph.connections.values.toList();Selection Operations
Select Node
// 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;
// Get actual node objects
final selectedNodes = selectedIds
.map((id) => controller.graph.getNode(id))
.whereType<Node<MyData>>()
.toList();Select All
void selectAll() {
for (final node in controller.graph.nodes.values) {
controller.selectNode(node.id, multiSelect: true);
}
}Viewport Operations
Pan To Position
controller.panTo(Offset(500, 300));Zoom
// Set specific zoom level
controller.zoomTo(1.5);
// Zoom in
final currentZoom = controller.viewport.zoom;
controller.zoomTo(currentZoom * 1.2);
// Zoom out
controller.zoomTo(currentZoom * 0.8);Fit to Screen
controller.fitToScreen();Centers all nodes in the viewport.
Center on Node
void centerOnNode(String nodeId) {
final node = controller.graph.getNode(nodeId);
if (node == null) return;
final viewport = controller.viewport;
final centerX = node.position.value.dx + (node.size.width / 2);
final centerY = node.position.value.dy + (node.size.height / 2);
controller.panTo(Offset(
centerX - (viewport.width / 2),
centerY - (viewport.height / 2),
));
}Alignment Operations
Align multiple nodes relative to each other:
// Align to left edge
controller.alignNodes(
['node-1', 'node-2', 'node-3'],
NodeAlignment.left,
);
// Align to right edge
controller.alignNodes(
selectedNodeIds,
NodeAlignment.right,
);
// Align horizontal center
controller.alignNodes(
selectedNodeIds,
NodeAlignment.horizontalCenter,
);
// Align to top
controller.alignNodes(
selectedNodeIds,
NodeAlignment.top,
);
// Align to bottom
controller.alignNodes(
selectedNodeIds,
NodeAlignment.bottom,
);
// Align vertical center
controller.alignNodes(
selectedNodeIds,
NodeAlignment.verticalCenter,
);Distribution Operations
Evenly distribute nodes:
// Distribute horizontally
controller.distributeNodes(
selectedNodeIds,
DistributionAxis.horizontal,
);
// Distribute vertically
controller.distributeNodes(
selectedNodeIds,
DistributionAxis.vertical,
);Graph Queries
Get Graph Bounds
final bounds = controller.graph.getBounds();
final width = bounds.width;
final height = bounds.height;Find Connected Nodes
List<Node<T>> getConnectedNodes(String nodeId) {
final connections = controller.graph.connections.values
.where((c) => c.sourceNodeId == nodeId || c.targetNodeId == nodeId)
.toList();
final connectedIds = <String>{};
for (final conn in connections) {
connectedIds.add(conn.sourceNodeId);
connectedIds.add(conn.targetNodeId);
}
connectedIds.remove(nodeId);
return connectedIds
.map((id) => controller.graph.getNode(id))
.whereType<Node<T>>()
.toList();
}Get Upstream Nodes
List<Node<T>> getUpstreamNodes(String nodeId) {
final incoming = controller.graph.connections.values
.where((c) => c.targetNodeId == nodeId)
.toList();
return incoming
.map((c) => controller.graph.getNode(c.sourceNodeId))
.whereType<Node<T>>()
.toList();
}Get Downstream Nodes
List<Node<T>> getDownstreamNodes(String nodeId) {
final outgoing = controller.graph.connections.values
.where((c) => c.sourceNodeId == nodeId)
.toList();
return outgoing
.map((c) => controller.graph.getNode(c.targetNodeId))
.whereType<Node<T>>()
.toList();
}Batch Operations
Perform multiple operations efficiently:
void addMultipleNodes(List<Node<MyData>> nodes) {
for (final node in nodes) {
controller.addNode(node);
}
}
void removeMultipleNodes(List<String> nodeIds) {
for (final id in nodeIds) {
controller.removeNode(id);
}
}Clear Graph
Remove all nodes and connections:
void clearGraph() {
// Remove all connections first
final connectionIds = controller.graph.connections.keys.toList();
for (final id in connectionIds) {
controller.removeConnection(id);
}
// Remove all nodes
final nodeIds = controller.graph.nodes.keys.toList();
for (final id in nodeIds) {
controller.removeNode(id);
}
}Keyboard Shortcuts
Show the built-in shortcuts dialog:
controller.showShortcutsDialog(context);Reactive Updates
The controller uses MobX for reactive state management. Wrap your UI in Observer to automatically rebuild:
Observer(
builder: (_) {
final nodeCount = controller.graph.nodes.length;
final connectionCount = controller.graph.connections.length;
return Text('Nodes: $nodeCount, Connections: $connectionCount');
},
)Custom Controller Extensions
Extend the controller with your own methods:
extension MyControllerExtensions on NodeFlowController<MyData> {
void addProcessNode(Offset position, String label) {
final node = Node<MyData>(
id: 'proc-${DateTime.now().millisecondsSinceEpoch}',
type: 'process',
position: position,
size: Size(150, 80),
data: MyData(label: label),
inputPorts: [
Port(
id: 'in',
name: 'Input',
position: PortPosition.left,
type: PortType.target,
),
],
outputPorts: [
Port(
id: 'out',
name: 'Output',
position: PortPosition.right,
type: PortType.source,
),
],
);
addNode(node);
}
void duplicateNode(String nodeId) {
final original = graph.getNode(nodeId);
if (original == null) return;
final duplicate = Node<MyData>(
id: 'dup-${DateTime.now().millisecondsSinceEpoch}',
type: original.type,
position: original.position.value + Offset(50, 50),
size: original.size,
data: original.data,
inputPorts: original.inputPorts.map((p) => Port(
id: '${p.id}-dup',
name: p.name,
position: p.position,
type: p.type,
offset: p.offset,
multiConnections: p.multiConnections,
)).toList(),
outputPorts: original.outputPorts.map((p) => Port(
id: '${p.id}-dup',
name: p.name,
position: p.position,
type: p.type,
offset: p.offset,
multiConnections: p.multiConnections,
)).toList(),
);
addNode(duplicate);
}
}
// Usage
controller.addProcessNode(Offset(100, 100), 'My Process');
controller.duplicateNode('node-1');Performance Tips
- Batch Updates: Group multiple operations together
- Spatial Queries: Use
getNodesInRect()for viewport culling - Observer Scope: Keep
Observerwidgets focused and small - Dispose: Always dispose the controller when done
- Node Count: Monitor performance with large graphs (1000+ nodes)
Complete Example
class FlowEditorWithController extends StatefulWidget {
@override
State<FlowEditorWithController> createState() =>
_FlowEditorWithControllerState();
}
class _FlowEditorWithControllerState
extends State<FlowEditorWithController> {
late final NodeFlowController<MyData> controller;
@override
void initState() {
super.initState();
controller = NodeFlowController<MyData>();
_initializeGraph();
}
void _initializeGraph() {
// Add initial nodes
controller.addNode(Node<MyData>(
id: 'start',
type: 'start',
position: Offset(100, 100),
size: Size(100, 60),
data: MyData(label: 'Start'),
outputPorts: [Port(/* ... */)],
));
controller.addNode(Node<MyData>(
id: 'process',
type: 'process',
position: Offset(300, 100),
size: Size(150, 80),
data: MyData(label: 'Process'),
inputPorts: [Port(/* ... */)],
outputPorts: [Port(/* ... */)],
));
// Connect them
controller.addConnection(Connection(
id: 'conn-1',
sourceNodeId: 'start',
sourcePortId: 'start-out',
targetNodeId: 'process',
targetPortId: 'process-in',
));
// Fit to screen
controller.fitToScreen();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Observer(
builder: (_) => Text(
'Nodes: ${controller.graph.nodes.length}',
),
),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: _addNode,
),
IconButton(
icon: Icon(Icons.delete),
onPressed: _deleteSelected,
),
IconButton(
icon: Icon(Icons.fit_screen),
onPressed: controller.fitToScreen,
),
],
),
body: NodeFlowEditor<MyData>(
controller: controller,
nodeBuilder: (context, node) => MyNodeWidget(node: node),
enablePanning: true,
enableZooming: true,
),
);
}
void _addNode() {
final viewport = controller.viewport;
controller.addNode(Node<MyData>(
id: 'node-${DateTime.now().millisecondsSinceEpoch}',
type: 'process',
position: Offset(viewport.x + 200, viewport.y + 200),
size: Size(150, 80),
data: MyData(label: 'New Node'),
inputPorts: [Port(/* ... */)],
outputPorts: [Port(/* ... */)],
));
}
void _deleteSelected() {
final selected = controller.selectedNodeIds.toList();
for (final id in selected) {
controller.removeNode(id);
}
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}Next Steps
- Learn about Serialization
- Explore Event System
- See Examples