NodeFlowEditor

Complete API reference for the NodeFlowEditor widget

NodeFlowEditor

The NodeFlowEditor is the main widget for creating interactive node-based flow editors. It provides a full-featured canvas with support for nodes, connections, panning, zooming, and more.

Constructor

NodeFlowEditor<T>({
  Key? key,
  required NodeFlowController<T> controller,
  required Widget Function(BuildContext, Node<T>) nodeBuilder,
  NodeFlowTheme? theme,
  NodeFlowEvents<T>? events,
  NodeFlowConfig? config,
})

Required Parameters

controller

required NodeFlowController<T> controller

The controller that manages the graph state. Create it in your widget's state:

late final NodeFlowController<MyData> controller;

@override
void initState() {
  super.initState();
  controller = NodeFlowController<MyData>();
}

@override
void dispose() {
  controller.dispose();
  super.dispose();
}

nodeBuilder

required Widget Function(BuildContext, Node<T>) nodeBuilder

A function that builds the widget for each node. This is where you customize how nodes appear:

nodeBuilder: (context, node) {
  return Container(
    padding: EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: Colors.blue),
    ),
    child: Text(node.data.label),
  );
}

Optional Parameters

theme

NodeFlowTheme? theme

Customizes the visual appearance of the editor. See Theming.

theme: NodeFlowTheme(
  nodeTheme: NodeTheme.light,
  connectionStyle: ConnectionStyles.smoothstep,
  backgroundColor: Colors.grey[50]!,
  gridStyle: GridStyle.dots,
  gridColor: Colors.grey[300]!,
)

events

NodeFlowEvents<T>? events

Comprehensive event handling for all editor interactions. See Event System for complete documentation.

events: NodeFlowEvents(
  node: NodeEvents(
    onTap: (node) => print('Tapped: ${node.id}'),
    onDoubleTap: (node) => _editNode(node),
    onSelected: (node) => setState(() => _selected = node),
    onDragStop: (node) => _savePosition(node),
    onContextMenu: (node, pos) => _showMenu(node, pos),
  ),
)
events: NodeFlowEvents(
  connection: ConnectionEvents(
    onCreated: (conn) => print('Connected: ${conn.id}'),
    onDeleted: (conn) => print('Disconnected: ${conn.id}'),
    onBeforeComplete: (context) => _validateConnection(context),
  ),
)
events: NodeFlowEvents(
  viewport: ViewportEvents(
    onCanvasTap: (pos) => _addNodeAt(pos),
    onCanvasContextMenu: (pos) => _showCanvasMenu(pos),
    onMove: (viewport) => _updateMinimap(viewport),
  ),
)

config

NodeFlowConfig? config

Controls editor behavior and interaction settings:

config: NodeFlowConfig(
  enablePanning: true,
  enableZooming: true,
  enableSelection: true,
  enableNodeDragging: true,
  enableConnectionCreation: true,
  scrollToZoom: false,
  readOnly: false,
)
OptionDefaultDescription
enablePanningtrueAllow dragging the canvas
enableZoomingtrueAllow zoom in/out
enableSelectiontrueAllow selecting nodes
enableNodeDraggingtrueAllow dragging nodes
enableConnectionCreationtrueAllow creating connections
scrollToZoomfalseUse scroll wheel for zooming
readOnlyfalseDisable all editing

Use readOnly: true for viewer-only mode, or set individual flags for fine-grained control.

Complete Example

class MyFlowEditor extends StatefulWidget {
  @override
  State<MyFlowEditor> createState() => _MyFlowEditorState();
}

class _MyFlowEditorState extends State<MyFlowEditor> {
  late final NodeFlowController<MyNodeData> _controller;
  Node<MyNodeData>? _selectedNode;

  @override
  void initState() {
    super.initState();
    _controller = NodeFlowController<MyNodeData>();
    _initializeGraph();
  }

  void _initializeGraph() {
    final node1 = Node<MyNodeData>(
      id: 'node-1',
      type: 'start',
      position: Offset(100, 100),
      size: Size(150, 80),
      data: MyNodeData(label: 'Start'),
      outputPorts: [
        Port(id: 'node-1-out', name: 'Output'),
      ],
    );
    _controller.addNode(node1);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Node Flow Editor'),
        actions: [
          IconButton(icon: Icon(Icons.add), onPressed: _addNode),
          if (_selectedNode != null)
            IconButton(icon: Icon(Icons.delete), onPressed: _deleteSelectedNode),
        ],
      ),
      body: Row(
        children: [
          Expanded(
            flex: 3,
            child: NodeFlowEditor<MyNodeData>(
              controller: _controller,
              theme: NodeFlowTheme.light,
              nodeBuilder: (context, node) => _buildNode(node),
              events: NodeFlowEvents(
                node: NodeEvents(
                  onSelected: (node) => setState(() => _selectedNode = node),
                  onDoubleTap: (node) => _editNode(node),
                  onContextMenu: (node, pos) => _showNodeMenu(node, pos),
                ),
                connection: ConnectionEvents(
                  onCreated: (conn) => _showSnackBar('Connection created'),
                  onDeleted: (conn) => _showSnackBar('Connection deleted'),
                ),
                viewport: ViewportEvents(
                  onCanvasTap: (pos) => _controller.clearSelection(),
                ),
              ),
              config: NodeFlowConfig(
                enablePanning: true,
                enableZooming: true,
              ),
            ),
          ),
          if (_selectedNode != null)
            SizedBox(
              width: 300,
              child: _buildPropertiesPanel(),
            ),
        ],
      ),
    );
  }

  Widget _buildNode(Node<MyNodeData> node) {
    return Container(
      padding: EdgeInsets.all(12),
      child: Text(
        node.data.label,
        style: TextStyle(fontWeight: FontWeight.bold),
      ),
    );
  }

  Widget _buildPropertiesPanel() {
    return Container(
      color: Colors.grey[100],
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Properties', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 16),
          Text('Node ID: ${_selectedNode!.id}'),
          Text('Type: ${_selectedNode!.type}'),
          SizedBox(height: 16),
          ElevatedButton(onPressed: _deleteSelectedNode, child: Text('Delete')),
        ],
      ),
    );
  }

  void _addNode() {
    final node = Node<MyNodeData>(
      id: 'node-${DateTime.now().millisecondsSinceEpoch}',
      type: 'process',
      position: Offset(200, 200),
      size: Size(150, 80),
      data: MyNodeData(label: 'New Node'),
      inputPorts: [Port(id: 'in-${DateTime.now().millisecondsSinceEpoch}', name: 'Input')],
      outputPorts: [Port(id: 'out-${DateTime.now().millisecondsSinceEpoch}', name: 'Output')],
    );
    _controller.addNode(node);
  }

  void _deleteSelectedNode() {
    if (_selectedNode != null) {
      _controller.removeNode(_selectedNode!.id);
      setState(() => _selectedNode = null);
    }
  }

  void _editNode(Node<MyNodeData> node) { /* Show edit dialog */ }
  void _showNodeMenu(Node<MyNodeData> node, Offset pos) { /* Show context menu */ }
  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Keyboard Shortcuts

The editor includes built-in keyboard shortcuts:

  • Delete / Backspace: Delete selected nodes
  • Ctrl+A / Cmd+A: Select all nodes
  • Escape: Clear selection
  • Arrow keys: Move selected nodes

See Keyboard Shortcuts for the complete list and customization options.

Best Practices

  1. Dispose Controller: Always dispose the controller in your widget's dispose method
  2. Responsive Layout: Use LayoutBuilder to make the editor responsive
  3. Loading State: Show a loading indicator while initializing the graph
  4. Error Handling: Wrap operations in try-catch blocks
  5. Performance: Keep node widgets lightweight
  6. State Management: Use controller APIs instead of directly modifying graph

See Also

On this page