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:
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:
| Interaction | How |
|---|---|
| Pan canvas | Right-click drag or Space + drag |
| Zoom | Mouse wheel or pinch gesture |
| Select node | Click on a node |
| Multi-select | Shift + click, or Shift + drag marquee |
| Drag node | Click and drag a node |
| Create connection | Drag from an output port to an input port |
| Delete | Select and press Delete or Backspace |
| Fit view | Press F key |
| Select all | Ctrl/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,
)