Vyuh Node Flow

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.

Video

NodeFlowEditor Overview

Quick video tour showing the editor in action: creating nodes, connecting ports, panning/zooming the canvas, selecting nodes, and using the minimap

PROTOTYPE PREVIEW

Built-in Capabilities

The NodeFlowEditor provides extensive functionality out-of-the-box:

Canvas & Viewport

CapabilityDescription
Infinite CanvasUnlimited workspace in all directions
Pan & ZoomSmooth viewport navigation with mouse/trackpad/touch
Zoom to FitFit all nodes or selection in viewport
Animated ViewportSmooth transitions between viewport states
Grid BackgroundOptional configurable grid with snap-to-grid
MinimapOptional overview for large graphs

Nodes

CapabilityDescription
Custom Node WidgetsFull control over node appearance via nodeBuilder
Node ShapesRectangle, circle, diamond, hexagon, or custom shapes
Drag & DropMove nodes with mouse/touch, supports multi-selection
SelectionSingle and multi-selection with Shift+click
Selection BoxMarquee selection by dragging on empty canvas
ResizeOptional resize handles for nodes
Z-Index LayeringControl node stacking order
Visibility ControlShow/hide individual nodes

Ports

CapabilityDescription
Custom Port WidgetsFull control over port appearance
Port PositionsLeft, right, top, bottom with offset support
Port TypesSource, target, or bidirectional
Multi-connectionConfigure ports for single or multiple connections
HighlightingVisual feedback during connection creation
LabelsOptional port labels with theming

Connections

CapabilityDescription
Interactive CreationDrag from port to create connections
Connection StylesBezier, smoothstep, step, or straight lines
Arrows & EndpointsConfigurable start/end markers
LabelsStart, center, and end labels on connections
Dashed LinesCustom dash patterns
SelectionClick to select connections
ValidationHook into connection creation for validation
Control PointsManual routing with user-defined waypoints

Annotations

CapabilityDescription
Sticky NotesResizable text notes anywhere on canvas
GroupsVisual containers around related nodes
MarkersIcon-based indicators for status/semantics
Node-followingAnnotations that move with their linked nodes
Selection & EditingSelect, move, resize annotations

Interaction

CapabilityDescription
Keyboard ShortcutsDelete, select all, escape, arrow keys, and more
Context MenusRight-click menus for nodes, connections, canvas
Auto-PanCanvas scrolls when dragging near edges
Cursor FeedbackDynamic cursors based on interaction state
Hit TestingAccurate click detection for overlapping elements

Alignment & Distribution

CapabilityDescription
Align Left/Right/Top/BottomAlign selected nodes to edges
Center Horizontal/VerticalCenter-align selected nodes
Distribute EvenlySpace nodes evenly horizontally or vertically
Snap to GridOptional grid snapping while dragging

Serialization

CapabilityDescription
Export GraphSerialize entire graph to JSON
Import GraphLoad graph from JSON
Custom DataFull support for your custom node data types
Annotations IncludedAnnotations serialize with the graph

Theming

CapabilityDescription
Light & Dark ThemesBuilt-in presets
Node ThemeColors, borders, shadows, selection styling
Connection ThemeStroke, color, endpoints, dash patterns
Port ThemeSize, colors, borders, highlighting
Annotation ThemeSelection styling, borders
Label ThemeFont, colors, backgrounds
Grid ThemeColors, spacing, line styles

All these capabilities work together seamlessly. For example, when you drag a node, connected edges update in real-time, annotations follow if linked, and the minimap updates accordingly.

Constructor

NodeFlowEditor<T>({
  Key? key,
  required NodeFlowController<T> controller,
  required Widget Function(BuildContext, Node<T>) nodeBuilder,
  required NodeFlowTheme theme,
  NodeShape? Function(BuildContext, Node<T>)? nodeShapeBuilder,
  Widget Function(BuildContext, Node<T>, Widget)? nodeContainerBuilder,
  PortBuilder<T>? portBuilder,
  LabelBuilder? labelBuilder,
  ConnectionStyleOverrides? Function(Connection)? connectionStyleResolver,
  NodeFlowEvents<T>? events,
  NodeFlowBehavior behavior = NodeFlowBehavior.design,
  bool scrollToZoom = true,
  bool showAnnotations = true,
})

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>(
    config: NodeFlowConfig(
      snapToGrid: true,
      gridSize: 20.0,
      autoPan: AutoPanConfig.normal,
    ),
  );
}

@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),
  );
}

theme

required NodeFlowTheme theme

The visual theme for the editor. This is required and controls all styling:

theme: NodeFlowTheme.light
// or
theme: NodeFlowTheme.dark
// or custom theme
theme: NodeFlowTheme(
  nodeTheme: NodeTheme(...),
  connectionTheme: ConnectionTheme(...),
  portTheme: PortTheme(...),
  backgroundColor: Colors.grey[50]!,
)

Optional Parameters

nodeShapeBuilder

NodeShape? Function(BuildContext, Node<T>)? nodeShapeBuilder

Determines the visual shape for each node. Return null for rectangular nodes.

nodeShapeBuilder: (context, node) {
  switch (node.type) {
    case 'Terminal':
      return CircleShape(
        fillColor: Colors.green,
        strokeColor: Colors.darkGreen,
        strokeWidth: 2.0,
      );
    case 'Decision':
      return DiamondShape(
        fillColor: Colors.yellow,
        strokeColor: Colors.black,
      );
    default:
      return null; // Rectangular node
  }
}
Image

Node Shapes Comparison

Side-by-side comparison showing different node shapes: rectangular (default), circle, diamond, and hexagon nodes with ports

PROTOTYPE PREVIEW

nodeContainerBuilder

Widget Function(BuildContext, Node<T>, Widget)? nodeContainerBuilder

Customize the node container wrapping. Receives the node content from nodeBuilder:

nodeContainerBuilder: (context, node, content) {
  return Container(
    decoration: BoxDecoration(
      border: Border.all(
        color: node.isSelected ? Colors.blue : Colors.grey,
        width: node.isSelected ? 2 : 1,
      ),
      boxShadow: node.isSelected
          ? [BoxShadow(color: Colors.blue.withOpacity(0.3), blurRadius: 8)]
          : null,
    ),
    child: content,
  );
}

portBuilder

PortBuilder<T>? portBuilder

Customize individual port widgets based on port data:

portBuilder: (context, node, port, isOutput, isConnected) {
  // Color ports based on data type
  final color = port.name.contains('error')
      ? Colors.red
      : null; // Use theme default

  return PortWidget(
    port: port,
    theme: Theme.of(context).extension<NodeFlowTheme>()!.portTheme,
    isConnected: isConnected,
    color: color,
  );
}

labelBuilder

LabelBuilder? labelBuilder

Customize connection label appearance:

labelBuilder: (context, connection, label, position) {
  return Positioned(
    left: position.left,
    top: position.top,
    child: Container(
      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: connection.data?['priority'] == 'high'
            ? Colors.orange.shade100
            : Colors.white,
        borderRadius: BorderRadius.circular(4),
      ),
      child: Text(label.text),
    ),
  );
}

connectionStyleResolver

ConnectionStyleOverrides? Function(Connection)? connectionStyleResolver

Override connection styles per-connection:

connectionStyleResolver: (connection) {
  if (connection.data?['type'] == 'error') {
    return ConnectionStyleOverrides(
      color: Colors.red,
      selectedColor: Colors.red.shade700,
      strokeWidth: 3.0,
    );
  }
  return null; // Use theme defaults
}

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),
  ),
)

behavior

NodeFlowBehavior behavior = NodeFlowBehavior.design

Controls what interactions are allowed. See Behavior Modes below.

ModeDescription
NodeFlowBehavior.designFull editing - create, modify, delete, drag, select, pan, zoom (default)
NodeFlowBehavior.previewNavigate and rearrange - drag, select, pan, zoom but no structural changes
NodeFlowBehavior.presentDisplay only - no interaction at all

scrollToZoom

bool scrollToZoom = true

When true, trackpad scroll gestures zoom the canvas. When false, scroll pans the canvas instead.

showAnnotations

bool showAnnotations = true

Whether to display annotations (sticky notes, markers, groups). When false, annotations remain in the graph data but are not rendered.

Behavior Modes

The NodeFlowBehavior enum controls what interactions are allowed:

Animation

Behavior Modes Comparison

Animated GIF showing the same graph in three modes: Design mode (full editing with connection creation), Preview mode (panning and rearranging only), and Present mode (static display with no interaction)

PROTOTYPE PREVIEW
// Full editing mode (default)
NodeFlowEditor(
  behavior: NodeFlowBehavior.design,
  // ...
)

// Preview mode - rearrange but no structural changes
NodeFlowEditor(
  behavior: NodeFlowBehavior.preview,
  // ...
)

// Presentation mode - display only
NodeFlowEditor(
  behavior: NodeFlowBehavior.present,
  // ...
)

Each behavior mode has specific capabilities:

Capabilitydesignpreviewpresent
canCreateYesNoNo
canUpdateYesNoNo
canDeleteYesNoNo
canDragYesYesNo
canSelectYesYesNo
canPanYesYesNo
canZoomYesYesNo

You can check capabilities programmatically:

if (controller.behavior.canDelete) {
  // Allow deletion
}

if (controller.behavior.canModify) {
  // Any CRUD operation allowed
}

if (controller.behavior.isInteractive) {
  // Any interaction allowed
}

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>(
      config: NodeFlowConfig(
        snapToGrid: true,
        gridSize: 20.0,
        autoPan: AutoPanConfig.normal,
        showMinimap: true,
      ),
    );
    _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,
              behavior: NodeFlowBehavior.design,
              scrollToZoom: true,
              showAnnotations: true,
              nodeBuilder: (context, node) => _buildNode(node),
              nodeShapeBuilder: (context, node) {
                if (node.type == 'start') {
                  return CircleShape(fillColor: Colors.green);
                }
                return null;
              },
              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.deselectAll(),
                ),
                onInit: () => _controller.fitToView(),
              ),
            ),
          ),
          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
  • F: Fit all nodes to view
  • ?: Show shortcuts dialog

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
  7. Behavior Modes: Use preview mode for run/debug views, present for thumbnails

See Also

On this page