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
Accessing Nodes
Access nodes directly from the controller:
// Get all nodes as a map
final allNodes = controller.nodes;
// Get specific node by ID
final node = controller.getNode('node-1');
// Get node count
final count = controller.nodeCount;
// Get all node IDs
final ids = controller.nodeIds;
// Get nodes by type
final processNodes = controller.getNodesByType('process');Accessing Connections
// Get all connections
final connections = controller.connections;
// Get specific connection
final connection = controller.getConnection('conn-1');
// Get connection count
final count = controller.connectionCount;
// Get connections for a specific node
final nodeConnections = controller.getConnectionsForNode('node-1');Viewport
Control the visible area of the canvas:
final viewport = controller.viewport;
// Get current zoom and pan
final zoom = controller.currentZoom;
final pan = controller.currentPan;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.
Move Node
// Move by delta
controller.moveNode('node-1', Offset(50, 0));
// Set absolute position
controller.setNodePosition('node-1', Offset(300, 200));Duplicate Node
final newNodeId = controller.duplicateNode('node-1');Get Node
final node = controller.getNode('node-1');Delete Multiple Nodes
controller.deleteNodes(['node-1', 'node-2', 'node-3']);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);Create Connection (Convenience Method)
final connection = controller.createConnection(
sourceNodeId: 'node-1',
sourcePortId: 'out-1',
targetNodeId: 'node-2',
targetPortId: 'in-1',
);Remove Connection
controller.removeConnection('conn-1');Get Connections
// Get connection by ID
final connection = controller.getConnection('conn-1');
// Get all connections for a node
final nodeConnections = controller.getConnectionsForNode('node-1');
// Get connections from a specific port
final fromPort = controller.getConnectionsFromPort('node-1', 'out-1');
// Get connections to a specific port
final toPort = controller.getConnectionsToPort('node-2', 'in-1');Selection Operations
Select Node
// Single selection (clears previous selection)
controller.selectNode('node-1');
// Toggle selection (add/remove from selection)
controller.selectNode('node-1', toggle: true);
controller.selectNode('node-2', toggle: true);Select Multiple Nodes
controller.selectNodes(['node-1', 'node-2', 'node-3']);Clear Selection
// Clear all selections (nodes, connections, annotations)
controller.clearSelection();
// Clear only node selection
controller.clearNodeSelection();
// Clear only connection selection
controller.clearConnectionSelection();Get Selected Nodes
final selectedIds = controller.selectedNodeIds;
// Get actual node objects
final selectedNodes = selectedIds
.map((id) => controller.getNode(id))
.whereType<Node<MyData>>()
.toList();Select All
controller.selectAllNodes();Check Selection State
final hasAnySelection = controller.hasSelection;
final isSelected = controller.isNodeSelected('node-1');Viewport Operations
Pan Viewport
// Pan by a delta
controller.panBy(ScreenOffset(Offset(100, 50)));Zoom
// Set specific zoom level
controller.zoomTo(1.5);
// Zoom by delta (positive = zoom in)
controller.zoomBy(0.1);Fit to View
// Fit all nodes in viewport
controller.fitToView();
// Fit only selected nodes
controller.fitSelectedNodes();Center on Node
// Center without changing zoom
controller.centerOnNode('node-1');
// Center with specific zoom
controller.centerOnNodeWithZoom('node-1', zoom: 1.5);
// Animate to node
controller.animateToNode('node-1', zoom: 1.0);Center Viewport
// Center on all nodes
controller.centerViewport();
// Center on selection
controller.centerOnSelection();
// Center on specific point
controller.centerOn(GraphOffset(Offset(500, 300)));Reset Viewport
controller.resetViewport();Coordinate Transformations
Convert between screen and graph coordinate systems:
// Screen to graph coordinates
final graphPos = controller.screenToGraph(ScreenPosition(screenPoint));
// Graph to screen coordinates
final screenPos = controller.graphToScreen(GraphPosition(graphPoint));
// Global (widget) to graph coordinates
final graphPos = controller.globalToGraph(ScreenPosition(globalPoint));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.toList(), NodeAlignment.right);
// Align horizontal center
controller.alignNodes(selectedNodeIds.toList(), NodeAlignment.horizontalCenter);
// Align to top
controller.alignNodes(selectedNodeIds.toList(), NodeAlignment.top);
// Align to bottom
controller.alignNodes(selectedNodeIds.toList(), NodeAlignment.bottom);
// Align vertical center
controller.alignNodes(selectedNodeIds.toList(), NodeAlignment.verticalCenter);Distribution Operations
Evenly distribute nodes:
// Distribute horizontally
controller.distributeNodesHorizontally(selectedNodeIds.toList());
// Distribute vertically
controller.distributeNodesVertically(selectedNodeIds.toList());Graph Queries
Get Graph Bounds
final bounds = controller.nodesBounds;
final width = bounds.width;
final height = bounds.height;Find Connected Nodes
// Get connections for a node and extract connected node IDs
final connections = controller.getConnectionsForNode('node-1');
final connectedIds = <String>{};
for (final conn in connections) {
connectedIds.add(conn.sourceNodeId);
connectedIds.add(conn.targetNodeId);
}
connectedIds.remove('node-1'); // Remove selfCycle Detection
final cycles = controller.detectCycles();
if (cycles.isNotEmpty) {
print('Found ${cycles.length} cycles in the graph');
}Orphan Nodes
final orphanNodes = controller.getOrphanNodes();
print('Found ${orphanNodes.length} unconnected nodes');Graph Import/Export
Export Graph
final graph = controller.exportGraph();
final json = graph.toJson((data) => data.toJson());
// Save to file or APILoad Graph
final json = jsonDecode(savedJson);
final graph = NodeGraph.fromJson(json, (map) => MyData.fromJson(map));
controller.loadGraph(graph);Clear Graph
controller.clearGraph();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.nodes.length;
final connectionCount = controller.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,
),
],
outputPorts: [
Port(
id: 'out',
name: 'Output',
position: PortPosition.right,
),
],
);
addNode(node);
}
}
// Usage
controller.addProcessNode(Offset(100, 100), 'My Process');Performance Tips
- Batch Updates: Group multiple operations together using
runInAction - 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(id: 'start-out', name: 'Next')],
));
controller.addNode(Node<MyData>(
id: 'process',
type: 'process',
position: Offset(300, 100),
size: Size(150, 80),
data: MyData(label: 'Process'),
inputPorts: [Port(id: 'process-in', name: 'Input')],
outputPorts: [Port(id: 'process-out', name: 'Output')],
));
// Connect them
controller.addConnection(Connection(
id: 'conn-1',
sourceNodeId: 'start',
sourcePortId: 'start-out',
targetNodeId: 'process',
targetPortId: 'process-in',
));
// Fit to view
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fitToView();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Observer(
builder: (_) => Text(
'Nodes: ${controller.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.fitToView,
),
],
),
body: NodeFlowEditor<MyData>(
controller: controller,
nodeBuilder: (context, node) => MyNodeWidget(node: node),
),
);
}
void _addNode() {
final center = controller.getViewportCenter();
controller.addNode(Node<MyData>(
id: 'node-${DateTime.now().millisecondsSinceEpoch}',
type: 'process',
position: center.offset,
size: Size(150, 80),
data: MyData(label: 'New Node'),
inputPorts: [Port(id: 'in', name: 'Input')],
outputPorts: [Port(id: 'out', name: 'Output')],
));
}
void _deleteSelected() {
controller.deleteNodes(controller.selectedNodeIds.toList());
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}Next Steps
- Learn about Serialization
- Explore Event System
- See Examples