Quick Start

Build your first node flow editor in 10 minutes

Quick Start

Build a fully functional flow editor with nodes, connections, and interactions.

What You'll Build

A flow editor with:

  • Three connected nodes
  • Drag-and-drop node positioning
  • Interactive connection creation
  • Pan and zoom navigation
  • Add new nodes with a button

The Code

Create the Controller

The NodeFlowController manages all state - nodes, connections, selection, and viewport.

late final NodeFlowController<String> controller;

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

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

The generic type <String> represents your node data. Use any type - Map<String, dynamic>, a custom class, or sealed classes for type-safe nodes.

Add Nodes

Create nodes with positions, sizes, and ports.

void _setupGraph() {
  // Start node
  controller.addNode(Node<String>(
    id: 'start',
    type: 'input',
    position: const Offset(100, 100),
    size: const Size(140, 70),
    data: 'Start',
    outputPorts: const [
      Port(id: 'out', position: PortPosition.right),
    ],
  ));

  // Process node
  controller.addNode(Node<String>(
    id: 'process',
    type: 'default',
    position: const Offset(320, 100),
    size: const Size(140, 70),
    data: 'Process',
    inputPorts: const [
      Port(id: 'in', position: PortPosition.left),
    ],
    outputPorts: const [
      Port(id: 'out', position: PortPosition.right),
    ],
  ));

  // End node
  controller.addNode(Node<String>(
    id: 'end',
    type: 'output',
    position: const Offset(540, 100),
    size: const Size(140, 70),
    data: 'End',
    inputPorts: const [
      Port(id: 'in', position: PortPosition.left),
    ],
  ));
}

Connect Nodes

Create connections between ports.

// Connect start -> process
controller.addConnection(Connection(
  id: 'conn-1',
  sourceNodeId: 'start',
  sourcePortId: 'out',
  targetNodeId: 'process',
  targetPortId: 'in',
));

// Connect process -> end
controller.addConnection(Connection(
  id: 'conn-2',
  sourceNodeId: 'process',
  sourcePortId: 'out',
  targetNodeId: 'end',
  targetPortId: 'in',
));

Build the Editor

Use NodeFlowEditor with a nodeBuilder callback.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('My Flow Editor'),
      actions: [
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: _addNode,
        ),
      ],
    ),
    body: NodeFlowEditor<String>(
      controller: controller,
      theme: NodeFlowTheme.light,
      nodeBuilder: (context, node) => Center(
        child: Text(
          node.data,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
      ),
    ),
  );
}

Complete Example

Here's the full working code:

my_flow_editor.dart
import 'package:flutter/material.dart';
import 'package:vyuh_node_flow/vyuh_node_flow.dart';

void main() {
  runApp(const MaterialApp(home: MyFlowEditor()));
}

class MyFlowEditor extends StatefulWidget {
  const MyFlowEditor({super.key});

  @override
  State<MyFlowEditor> createState() => _MyFlowEditorState();
}

class _MyFlowEditorState extends State<MyFlowEditor> {
  late final NodeFlowController<String> controller;

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

  void _setupGraph() {
    // Add nodes
    controller.addNode(Node<String>(
      id: 'start',
      type: 'input',
      position: const Offset(100, 100),
      size: const Size(140, 70),
      data: 'Start',
      outputPorts: const [
        Port(id: 'out', position: PortPosition.right),
      ],
    ));

    controller.addNode(Node<String>(
      id: 'process',
      type: 'default',
      position: const Offset(320, 100),
      size: const Size(140, 70),
      data: 'Process',
      inputPorts: const [
        Port(id: 'in', position: PortPosition.left),
      ],
      outputPorts: const [
        Port(id: 'out', position: PortPosition.right),
      ],
    ));

    controller.addNode(Node<String>(
      id: 'end',
      type: 'output',
      position: const Offset(540, 100),
      size: const Size(140, 70),
      data: 'End',
      inputPorts: const [
        Port(id: 'in', position: PortPosition.left),
      ],
    ));

    // Add connections
    controller.addConnection(Connection(
      id: 'conn-1',
      sourceNodeId: 'start',
      sourcePortId: 'out',
      targetNodeId: 'process',
      targetPortId: 'in',
    ));

    controller.addConnection(Connection(
      id: 'conn-2',
      sourceNodeId: 'process',
      sourcePortId: 'out',
      targetNodeId: 'end',
      targetPortId: 'in',
    ));
  }

  void _addNode() {
    final id = 'node-${DateTime.now().millisecondsSinceEpoch}';
    controller.addNode(Node<String>(
      id: id,
      type: 'default',
      position: const Offset(200, 250),
      size: const Size(140, 70),
      data: 'New Node',
      inputPorts: [Port(id: '$id-in', position: PortPosition.left)],
      outputPorts: [Port(id: '$id-out', position: PortPosition.right)],
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Flow Editor'),
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            tooltip: 'Add Node',
            onPressed: _addNode,
          ),
          IconButton(
            icon: const Icon(Icons.fit_screen),
            tooltip: 'Fit View',
            onPressed: () => controller.fitToView(),
          ),
        ],
      ),
      body: NodeFlowEditor<String>(
        controller: controller,
        theme: NodeFlowTheme.light,
        nodeBuilder: (context, node) => Center(
          child: Text(
            node.data,
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
        ),
        events: NodeFlowEvents(
          node: NodeEvents(
            onTap: (node) => debugPrint('Tapped: ${node.data}'),
          ),
          connection: ConnectionEvents(
            onCreated: (conn) => debugPrint('Connected: ${conn.id}'),
          ),
        ),
      ),
    );
  }

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

Interactions Out of the Box

Your editor now supports:

InteractionHow
Pan canvasRight-click drag or Space + drag
ZoomMouse wheel or pinch gesture
Select nodeClick on a node
Multi-selectShift + click, or Shift + drag marquee
Drag nodeClick and drag a node
Create connectionDrag from an output port to an input port
DeleteSelect and press Delete or Backspace
Fit viewPress F key
Select allCtrl/Cmd + A

Press ? to open the keyboard shortcuts viewer and see all available shortcuts.

Customization Options

NodeFlowEditor<String>(
  controller: controller,
  theme: NodeFlowTheme.dark, // or NodeFlowTheme.light
  // Or create a custom theme:
  theme: NodeFlowTheme(
    backgroundColor: Colors.grey.shade900,
    nodeTheme: NodeTheme(
      backgroundColor: Colors.blue.shade800,
      borderColor: Colors.blue.shade400,
    ),
    connectionTheme: ConnectionTheme(
      style: ConnectionStyles.bezier,
      color: Colors.blue,
    ),
    gridTheme: GridTheme(
      style: GridStyles.dots,
      color: Colors.grey.shade700,
    ),
  ),
)
NodeFlowEditor<String>(
  controller: controller,
  events: NodeFlowEvents(
    node: NodeEvents(
      onTap: (node) => print('Tapped: ${node.id}'),
      onDoubleTap: (node) => _editNode(node),
      onDragStop: (node) => _savePosition(node),
    ),
    connection: ConnectionEvents(
      onCreated: (conn) => print('Created: ${conn.id}'),
      onDeleted: (conn) => print('Deleted: ${conn.id}'),
    ),
    viewport: ViewportEvents(
      onCanvasTap: (pos) => _handleCanvasTap(pos),
    ),
    onSelectionChange: (state) => _updateToolbar(state),
  ),
)
NodeFlowEditor<String>(
  controller: controller,
  // Toggle features on/off
  enablePanning: true,
  enableZooming: true,
  enableSelection: true,
  enableNodeDragging: true,
  enableConnectionCreation: true,
  enableNodeDeletion: true,
  scrollToZoom: true,
  showAnnotations: true,
)

Next Steps

On this page