Node

API reference for the Node class

Node

The Node class represents an individual node in the graph. Nodes contain custom data, ports for connections, and use MobX observables for reactive state management.

Constructor

Node<T>({
  required String id,
  required String type,
  required Offset position,
  required T data,
  Size? size,
  List<Port> inputPorts = const [],
  List<Port> outputPorts = const [],
  int initialZIndex = 0,
})

Properties

PropertyTypeDefaultDescription
idStringrequiredUnique identifier
typeStringrequiredNode type for categorization
positionObservable<Offset>requiredTop-left position on canvas
sizeObservable<Size>Size(150, 100)Width and height
dataTrequiredCustom data payload
inputPortsObservableList<Port>[]Input connection ports
outputPortsObservableList<Port>[]Output connection ports
zIndexObservable<int>0Stacking order (higher = on top)
selectedObservable<bool>falseSelection state (not serialized)
draggingObservable<bool>falseDragging state (not serialized)
visualPositionObservable<Offset>same as positionPosition for rendering (may differ with snap-to-grid)

Properties like position, size, and selected are MobX observables. Access their values using .value (e.g., node.position.value).

Convenience Properties

PropertyTypeDescription
currentZIndexintGet/set z-index value
isSelectedboolGet/set selection state
isDraggingboolGet/set dragging state
allPortsList<Port>Combined list of input and output ports

Examples

final node = Node<String>(
  id: 'node-1',
  type: 'process',
  position: Offset(100, 100),
  data: 'My Node',
);
final node = Node<TaskData>(
  id: 'task-1',
  type: 'task',
  position: Offset(100, 100),
  size: Size(180, 100),
  data: TaskData(
    title: 'Process Data',
    status: TaskStatus.pending,
  ),
  inputPorts: [
    Port(id: 'task-1-in', name: 'Input', position: PortPosition.left),
  ],
  outputPorts: [
    Port(id: 'task-1-success', name: 'Success', position: PortPosition.right),
    Port(id: 'task-1-error', name: 'Error', position: PortPosition.right),
  ],
);
final startNode = Node<WorkflowData>(
  id: 'start',
  type: 'trigger',
  position: Offset(50, 100),
  size: Size(80, 80),
  data: WorkflowData(label: 'Start'),
  outputPorts: [
    Port(id: 'start-out', name: 'Begin', position: PortPosition.right),
  ],
  initialZIndex: 1,
);

final processNode = Node<WorkflowData>(
  id: 'process',
  type: 'action',
  position: Offset(200, 100),
  size: Size(150, 80),
  data: WorkflowData(label: 'Process'),
  inputPorts: [
    Port(id: 'process-in', name: 'Input', position: PortPosition.left),
  ],
  outputPorts: [
    Port(id: 'process-out', name: 'Output', position: PortPosition.right),
  ],
);

Methods

Port Management

findPort

Find a port by ID in either input or output ports.

Port? findPort(String portId)

Example:

final port = node.findPort('task-1-in');
if (port != null) {
  print('Found port: ${port.name}');
}

addInputPort / addOutputPort

Add ports to an existing node.

void addInputPort(Port port)
void addOutputPort(Port port)

Example:

node.addInputPort(Port(id: 'new-input', name: 'New Input'));
node.addOutputPort(Port(id: 'new-output', name: 'New Output'));

removeInputPort / removeOutputPort / removePort

Remove ports from a node.

bool removeInputPort(String portId)
bool removeOutputPort(String portId)
bool removePort(String portId)  // Searches both input and output

Returns true if the port was found and removed.

updateInputPort / updateOutputPort / updatePort

Update an existing port.

bool updateInputPort(String portId, Port updatedPort)
bool updateOutputPort(String portId, Port updatedPort)
bool updatePort(String portId, Port updatedPort)  // Searches both

Returns true if the port was found and updated.

Geometry Methods

getBounds

Get the node's bounding rectangle.

Rect getBounds()

Example:

final bounds = node.getBounds();
print('Node area: ${bounds.width} x ${bounds.height}');

containsPoint

Check if a point is within the node's rectangular bounds.

bool containsPoint(Offset point)

getPortPosition

Get the connection point for a port in graph coordinates.

Offset getPortPosition(
  String portId, {
  required Size portSize,
  NodeShape? shape,
})

getVisualPortPosition

Get the visual position where a port should be rendered within the node container.

Offset getVisualPortPosition(
  String portId, {
  required Size portSize,
  NodeShape? shape,
})

Visual Position

setVisualPosition

Update the visual position (used with snap-to-grid).

void setVisualPosition(Offset snappedPosition)

Serialization

toJson

Serialize to JSON.

Map<String, dynamic> toJson(Object? Function(T value) toJsonT)

Example:

final json = node.toJson((data) => data.toJson());

fromJson

Create from JSON.

factory Node.fromJson(
  Map<String, dynamic> json,
  T Function(Object? json) fromJsonT,
)

Example:

final node = Node.fromJson(json, (data) => MyData.fromJson(data as Map<String, dynamic>));

Custom Data

The data property holds your custom data type. Define a class that implements toJson and fromJson for serialization:

class TaskData {
  final String title;
  final String description;
  final TaskStatus status;
  final DateTime? dueDate;

  TaskData({
    required this.title,
    this.description = '',
    this.status = TaskStatus.pending,
    this.dueDate,
  });

  Map<String, dynamic> toJson() => {
    'title': title,
    'description': description,
    'status': status.name,
    'dueDate': dueDate?.toIso8601String(),
  };

  factory TaskData.fromJson(Map<String, dynamic> json) => TaskData(
    title: json['title'],
    description: json['description'] ?? '',
    status: TaskStatus.values.byName(json['status']),
    dueDate: json['dueDate'] != null ? DateTime.parse(json['dueDate']) : null,
  );
}

enum TaskStatus { pending, inProgress, completed, failed }

Node Shapes

Node shapes are handled separately through the NodeShape abstract class for custom rendering. The library provides these built-in shapes:

  • CircleShape - Circular nodes
  • DiamondShape - Diamond/rhombus shaped nodes
  • HexagonShape - Hexagonal nodes

Custom shapes can be created by extending NodeShape and implementing the required methods for path generation and port anchor calculation.

Shapes affect how nodes are rendered and where ports are positioned. The default rectangular rendering is used when no shape is specified.

Node Widget

The nodeBuilder function receives the node and returns the visual representation:

NodeFlowEditor<TaskData>(
  controller: controller,
  nodeBuilder: (context, node) {
    return Container(
      padding: EdgeInsets.all(12),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            node.data.title,
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 4),
          StatusBadge(status: node.data.status),
        ],
      ),
    );
  },
)

The node widget is rendered inside the node's bounds. The NodeFlowTheme handles background, border, and shadow. Your widget only needs to render the content.

Reactive Updates

Node properties use MobX observables. Use Observer widgets to react to changes:

Observer(
  builder: (_) {
    return Text('Position: ${node.position.value}');
  },
)

Or use MobX's runInAction for batch updates:

runInAction(() {
  node.position.value = Offset(200, 200);
  node.selected.value = true;
});

Best Practices

  1. Unique IDs: Use UUIDs or timestamps for reliable uniqueness
  2. Appropriate Sizing: Size nodes to fit their content plus padding
  3. Type Categorization: Use type to differentiate node categories (required)
  4. Port Organization: Group related ports and use consistent naming
  5. Dispose: Call node.dispose() when removing nodes (currently a no-op but good practice)

On this page