NodeFlowController
API reference for the NodeFlowController class
NodeFlowController
The NodeFlowController manages all graph state including nodes, connections, selection, and viewport. It's the central point for programmatic graph manipulation.
Constructor
NodeFlowController<T>({
GraphViewport? initialViewport,
NodeFlowConfig? config,
})| Parameter | Type | Description |
|---|---|---|
initialViewport | GraphViewport? | Initial viewport position and zoom |
config | NodeFlowConfig? | Behavioral configuration (snap-to-grid, zoom limits, etc.) |
Node Operations
addNode
Add a node to the graph.
void addNode(Node<T> node)Example:
final node = Node<MyData>(
id: 'node-1',
position: Offset(100, 100),
size: Size(150, 80),
data: MyData(label: 'Process'),
inputPorts: [Port(id: 'in-1', name: 'Input')],
outputPorts: [Port(id: 'out-1', name: 'Output')],
);
controller.addNode(node);removeNode
Remove a node and all its connections.
void removeNode(String nodeId)Removing a node automatically removes all connections to and from that node, and removes the node from any group annotations.
moveNode
Move a node by a delta offset.
void moveNode(String nodeId, Offset delta)moveSelectedNodes
Move all selected nodes by a delta offset.
void moveSelectedNodes(Offset delta)setNodeSize
Update a node's size.
void setNodeSize(String nodeId, Size size)setNodePorts
Replace a node's ports.
void setNodePorts(String nodeId, {
List<Port>? inputPorts,
List<Port>? outputPorts,
})addInputPort / addOutputPort
Add ports to an existing node.
void addInputPort(String nodeId, Port port)
void addOutputPort(String nodeId, Port port)removePort
Remove a port and all its connections.
void removePort(String nodeId, String portId)getNode
Get a node by ID.
Node<T>? getNode(String nodeId)Connection Operations
addConnection
Create a connection between ports.
void addConnection(Connection connection)Example:
final connection = Connection(
id: 'conn-1',
sourceNodeId: 'node-1',
sourcePortId: 'out-1',
targetNodeId: 'node-2',
targetPortId: 'in-1',
);
controller.addConnection(connection);removeConnection
Remove a connection by ID.
void removeConnection(String connectionId)getConnectionsForNode
Get all connections for a node.
List<Connection> getConnectionsForNode(String nodeId)connections
Access all connections (read-only list).
List<Connection> get connectionsControl Points
Connections support user-defined control points for custom routing.
addControlPoint
void addControlPoint(String connectionId, Offset position, {int? index})updateControlPoint
void updateControlPoint(String connectionId, int index, Offset position)removeControlPoint
void removeControlPoint(String connectionId, int index)clearControlPoints
void clearControlPoints(String connectionId)Selection
selectNode
Select a node. Use toggle: true for multi-select behavior.
void selectNode(String nodeId, {bool toggle = false})selectNodes
Select multiple nodes.
void selectNodes(List<String> nodeIds, {bool toggle = false})selectConnection
Select a connection.
void selectConnection(String connectionId, {bool toggle = false})clearSelection
Clear all selections (nodes, connections, annotations).
void clearSelection()clearNodeSelection / clearConnectionSelection
Clear specific selection types.
void clearNodeSelection()
void clearConnectionSelection()selectedNodeIds
Get IDs of selected nodes.
Set<String> get selectedNodeIdshasSelection
Check if anything is selected.
bool get hasSelectionisNodeSelected
Check if a specific node is selected.
bool isNodeSelected(String nodeId)Viewport
viewport
Current viewport state.
GraphViewport get viewportReturns GraphViewport with:
x,y- Pan offsetzoom- Zoom level
currentZoom / currentPan
Access current zoom level and pan position.
double get currentZoom
ScreenOffset get currentPansetViewport
Set viewport directly.
void setViewport(GraphViewport viewport)panBy
Pan viewport by a delta.
void panBy(ScreenOffset delta)zoomBy
Zoom by a delta amount (positive = zoom in), keeping the viewport center fixed.
void zoomBy(double delta)zoomTo
Set zoom to a specific level.
void zoomTo(double zoom)fitToView
Fit all nodes in the viewport with padding.
void fitToView()fitSelectedNodes
Fit only selected nodes in the viewport with padding.
void fitSelectedNodes()centerOnNode
Center viewport on a specific node without changing zoom.
void centerOnNode(String nodeId)centerOnSelection
Center viewport on the geometric center of selected nodes.
void centerOnSelection()centerViewport
Center viewport on the geometric center of all nodes.
void centerViewport()centerOn
Center viewport on a specific point in graph coordinates.
void centerOn(GraphOffset point)getViewportCenter
Get the center point of the viewport in graph coordinates.
GraphPosition getViewportCenter()resetViewport
Reset viewport to zoom 1.0 and center on all nodes.
void resetViewport()Coordinate Transformations
Node Flow uses typed coordinate systems to prevent accidentally mixing screen and graph coordinates.
globalToGraph
Convert global screen position to graph coordinates.
GraphPosition globalToGraph(ScreenPosition globalPosition)Example:
// In a gesture callback
final graphPos = controller.globalToGraph(
ScreenPosition(details.globalPosition)
);graphToScreen
Convert graph coordinates to screen coordinates.
ScreenPosition graphToScreen(GraphPosition graphPoint)screenToGraph
Convert screen coordinates to graph coordinates.
GraphPosition screenToGraph(ScreenPosition screenPoint)Example:
// Convert mouse position to graph coordinates
final graphPos = controller.screenToGraph(
ScreenPosition(event.localPosition)
);Visibility & Bounds
viewportExtent
Get the visible area in graph coordinates.
GraphRect get viewportExtentviewportScreenBounds
Get the viewport bounds in global screen coordinates.
ScreenRect get viewportScreenBoundsisPointVisible
Check if a graph coordinate point is visible.
bool isPointVisible(GraphPosition graphPoint)isRectVisible
Check if a graph coordinate rectangle is visible (useful for culling).
bool isRectVisible(GraphRect graphRect)selectedNodesBounds
Get the bounding rectangle of all selected nodes.
GraphRect? get selectedNodesBoundsnodesBounds
Get the bounding rectangle of all nodes.
GraphRect get nodesBoundsViewport Animations
All animation methods use smooth easing and accept optional duration and curve parameters.
Viewport Animation Methods
Side-by-side comparison showing animateToNode() (smooth pan+zoom to a specific node), animateToBounds() (fit selection with animation), and animateToScale() (smooth zoom to target level). Each animation uses easeInOut curve.
animateToViewport
Animate to a target viewport state.
void animateToViewport(
GraphViewport target, {
Duration duration = const Duration(milliseconds: 400),
Curve curve = Curves.easeInOut,
})Example:
controller.animateToViewport(
GraphViewport(x: 100, y: 50, zoom: 1.5),
duration: Duration(milliseconds: 300),
);animateToNode
Animate to center on a specific node.
void animateToNode(
String nodeId, {
double? zoom = 1.0,
Duration duration = const Duration(milliseconds: 400),
Curve curve = Curves.easeInOut,
})Example:
// Animate to node with zoom
controller.animateToNode('node-123', zoom: 1.5);
// Animate to node, keeping current zoom
controller.animateToNode('node-123', zoom: null);animateToPosition
Animate to center on a specific graph position.
void animateToPosition(
GraphOffset position, {
double? zoom,
Duration duration = const Duration(milliseconds: 400),
Curve curve = Curves.easeInOut,
})animateToBounds
Animate to fit a bounding rectangle with padding.
void animateToBounds(
GraphRect bounds, {
double padding = 50.0,
Duration duration = const Duration(milliseconds: 400),
Curve curve = Curves.easeInOut,
})Example:
// Animate to fit selected nodes
final bounds = controller.selectedNodesBounds;
if (bounds != null) {
controller.animateToBounds(bounds, padding: 100);
}animateToScale
Animate to a specific zoom level, keeping the center fixed.
void animateToScale(
double scale, {
Duration duration = const Duration(milliseconds: 400),
Curve curve = Curves.easeInOut,
})centerOnNodeWithZoom
Immediately center on a node with a specific zoom (non-animated).
void centerOnNodeWithZoom(String nodeId, {double zoom = 1.0})Mouse Position
mousePositionWorld
Get the current mouse position in graph coordinates.
GraphPosition? get mousePositionWorldReturns null if the mouse is outside the canvas.
Graph Operations
loadGraph
Load a complete graph (nodes, connections, annotations, viewport).
void loadGraph(NodeGraph<T> graph)exportGraph
Export current graph state.
NodeGraph<T> exportGraph()Example:
// Save
final graph = controller.exportGraph();
final json = graph.toJson((data) => data.toJson());
await saveToFile(jsonEncode(json));
// Load
final json = jsonDecode(await loadFromFile());
final graph = NodeGraph.fromJson(json, (map) => MyData.fromJson(map));
controller.loadGraph(graph);clearGraph
Remove all nodes, connections, and annotations.
void clearGraph()Alignment & Distribution
alignNodes
Align nodes to a specific edge or center.
void alignNodes(List<String> nodeIds, NodeAlignment alignment)| Alignment | Description |
|---|---|
NodeAlignment.left | Align to left edge |
NodeAlignment.right | Align to right edge |
NodeAlignment.top | Align to top edge |
NodeAlignment.bottom | Align to bottom edge |
NodeAlignment.horizontalCenter | Center horizontally |
NodeAlignment.verticalCenter | Center vertically |
distributeNodesHorizontally / distributeNodesVertically
Distribute nodes evenly.
void distributeNodesHorizontally(List<String> nodeIds)
void distributeNodesVertically(List<String> nodeIds)Annotations
Annotations are accessed via controller.annotations:
addAnnotation / removeAnnotation
controller.annotations.addAnnotation(annotation);
controller.annotations.removeAnnotation(annotationId);getAnnotation
Annotation? annotation = controller.annotations.getAnnotation(annotationId);selectAnnotation / clearAnnotationSelection
controller.annotations.selectAnnotation(annotationId, toggle: false);
controller.annotations.clearAnnotationSelection();Factory Methods
Convenience methods for creating common annotation types (on controller.annotations):
// Create a sticky note
final sticky = controller.annotations.createStickyAnnotation(
id: 'sticky-1',
position: Offset(100, 100),
text: 'Remember this!',
width: 200.0,
height: 100.0,
color: Colors.yellow,
);
controller.annotations.addAnnotation(sticky);
// Create a group annotation with position and size
final group = controller.annotations.createGroupAnnotation(
id: 'group-1',
title: 'My Group',
position: Offset(50, 50),
size: Size(300, 200),
color: Colors.blue,
);
controller.annotations.addAnnotation(group);
// Create a group around existing nodes
final groupAroundNodes = controller.annotations.createGroupAnnotationAroundNodes(
id: 'group-2',
title: 'Node Group',
nodeIds: {'node-1', 'node-2'},
padding: EdgeInsets.all(20.0),
color: Colors.green,
);
controller.annotations.addAnnotation(groupAroundNodes);
// Create a marker
final marker = controller.annotations.createMarkerAnnotation(
id: 'marker-1',
position: Offset(200, 200),
markerType: MarkerType.info,
size: 24.0,
color: Colors.red,
tooltip: 'Important point',
);
controller.annotations.addAnnotation(marker);Lifecycle
dispose
Dispose the controller and release resources.
void dispose()Always call dispose() when the controller is no longer needed to prevent memory leaks.
Complete Example
class WorkflowEditor extends StatefulWidget {
@override
State<WorkflowEditor> createState() => _WorkflowEditorState();
}
class _WorkflowEditorState extends State<WorkflowEditor> {
late final NodeFlowController<WorkflowData> controller;
@override
void initState() {
super.initState();
controller = NodeFlowController<WorkflowData>();
_setupGraph();
}
void _setupGraph() {
controller.addNode(Node(
id: 'start',
position: Offset(100, 100),
size: Size(120, 60),
data: WorkflowData(label: 'Start', type: 'trigger'),
outputPorts: [Port(id: 'start-out', name: 'Next')],
));
controller.addNode(Node(
id: 'process',
position: Offset(300, 100),
size: Size(120, 60),
data: WorkflowData(label: 'Process', type: 'action'),
inputPorts: [Port(id: 'process-in', name: 'Input')],
outputPorts: [Port(id: 'process-out', name: 'Output')],
));
controller.addConnection(Connection(
id: 'conn-1',
sourceNodeId: 'start',
sourcePortId: 'start-out',
targetNodeId: 'process',
targetPortId: 'process-in',
));
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fitToView();
});
}
void _addNode() {
final id = 'node-${DateTime.now().millisecondsSinceEpoch}';
controller.addNode(Node(
id: id,
position: Offset(200, 200),
size: Size(120, 60),
data: WorkflowData(label: 'New Node', type: 'action'),
inputPorts: [Port(id: '$id-in', name: 'Input')],
outputPorts: [Port(id: '$id-out', name: 'Output')],
));
controller.selectNode(id);
}
void _deleteSelected() {
for (final nodeId in controller.selectedNodeIds.toList()) {
controller.removeNode(nodeId);
}
}
void _saveGraph() async {
final graph = controller.exportGraph();
final json = graph.toJson((data) => data.toJson());
// Save to file or API
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Workflow Editor'),
actions: [
IconButton(icon: Icon(Icons.add), onPressed: _addNode),
IconButton(icon: Icon(Icons.delete), onPressed: _deleteSelected),
IconButton(icon: Icon(Icons.fit_screen), onPressed: controller.fitToView),
IconButton(icon: Icon(Icons.save), onPressed: _saveGraph),
],
),
body: NodeFlowEditor<WorkflowData>(
controller: controller,
theme: NodeFlowTheme.light,
nodeBuilder: (context, node) => Center(child: Text(node.data.label)),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}