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

  1. Batch Updates: Group multiple operations together
  2. Spatial Queries: Use getNodesInRect() for viewport culling
  3. Observer Scope: Keep Observer widgets focused and small
  4. Dispose: Always dispose the controller when done
  5. 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

On this page