Node Widget

Building custom node widgets for your flow editor

Node Widget

The Node Widget is what users see and interact with in your flow editor. You have complete control over how nodes appear through the nodeBuilder function.

Basic Node Widget

The simplest node widget is just a container with some text:

NodeFlowEditor<String>(
  controller: controller,
  nodeBuilder: (context, node) {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.blue),
      ),
      child: Text(node.data),
    );
  },
)

Widget Structure

A typical node widget has these parts:

  1. Container: Defines size, decoration, borders
  2. Content: Title, icon, description, data
  3. Interactivity: Gesture handlers for taps, double-taps
  4. State Indicators: Selection, hover, disabled states
Widget buildNodeWidget(BuildContext context, Node<MyData> node) {
  return Container(
    width: node.size.width,
    height: node.size.height,
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(8),
      border: Border.all(
        color: isSelected ? Colors.blue : Colors.grey[300]!,
        width: isSelected ? 2 : 1,
      ),
      boxShadow: [
        BoxShadow(
          color: Colors.black12,
          blurRadius: 8,
          offset: Offset(0, 4),
        ),
      ],
    ),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(getIconForType(node.type)),
        SizedBox(height: 8),
        Text(
          node.data.title,
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        if (node.data.description.isNotEmpty)
          Text(
            node.data.description,
            style: TextStyle(fontSize: 10, color: Colors.grey),
          ),
      ],
    ),
  );
}

Type-Based Widgets

Use the node type field to render different widgets:

nodeBuilder: (context, node) {
  switch (node.type) {
    case 'start':
      return StartNodeWidget(node: node);
    case 'process':
      return ProcessNodeWidget(node: node);
    case 'condition':
      return ConditionNodeWidget(node: node);
    case 'end':
      return EndNodeWidget(node: node);
    default:
      return DefaultNodeWidget(node: node);
  }
}

Example: Start Node

class StartNodeWidget extends StatelessWidget {
  final Node<MyData> node;

  const StartNodeWidget({required this.node});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: node.size.width,
      height: node.size.height,
      decoration: BoxDecoration(
        color: Colors.green[50],
        borderRadius: BorderRadius.circular(24),
        border: Border.all(color: Colors.green, width: 2),
      ),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.play_arrow, color: Colors.green, size: 32),
            SizedBox(height: 4),
            Text(
              'START',
              style: TextStyle(
                color: Colors.green[700],
                fontWeight: FontWeight.bold,
                fontSize: 12,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Example: Process Node

class ProcessNodeWidget extends StatelessWidget {
  final Node<ProcessData> node;

  const ProcessNodeWidget({required this.node});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: node.size.width,
      height: node.size.height,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.blue, width: 2),
        boxShadow: [
          BoxShadow(
            color: Colors.blue.withOpacity(0.1),
            blurRadius: 8,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: Padding(
        padding: EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(Icons.settings, size: 20, color: Colors.blue),
                SizedBox(width: 8),
                Expanded(
                  child: Text(
                    node.data.title,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 14,
                    ),
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ],
            ),
            if (node.data.description.isNotEmpty) ...[
              SizedBox(height: 8),
              Text(
                node.data.description,
                style: TextStyle(
                  fontSize: 11,
                  color: Colors.grey[600],
                ),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
            ],
          ],
        ),
      ),
    );
  }
}

Example: Condition Node

class ConditionNodeWidget extends StatelessWidget {
  final Node<ConditionData> node;

  const ConditionNodeWidget({required this.node});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: node.size.width,
      height: node.size.height,
      decoration: BoxDecoration(
        color: Colors.amber[50],
        border: Border.all(color: Colors.amber, width: 2),
      ),
      child: Stack(
        children: [
          // Diamond shape using CustomPaint
          CustomPaint(
            painter: DiamondPainter(color: Colors.amber[50]!),
            child: Container(),
          ),
          Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.help_outline, color: Colors.amber[900]),
                SizedBox(height: 4),
                Text(
                  node.data.condition,
                  style: TextStyle(
                    fontSize: 11,
                    fontWeight: FontWeight.bold,
                  ),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Interactive Nodes

Add interactivity to your nodes:

class InteractiveNodeWidget extends StatelessWidget {
  final Node<MyData> node;
  final NodeFlowController controller;

  const InteractiveNodeWidget({
    required this.node,
    required this.controller,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => _handleTap(context),
      onDoubleTap: () => _handleDoubleTap(context),
      onLongPress: () => _handleLongPress(context),
      child: Container(
        // Node UI
        child: Text(node.data.title),
      ),
    );
  }

  void _handleTap(BuildContext context) {
    controller.selectNode(node.id);
  }

  void _handleDoubleTap(BuildContext context) {
    // Open properties dialog
    showDialog(
      context: context,
      builder: (_) => NodePropertiesDialog(node: node),
    );
  }

  void _handleLongPress(BuildContext context) {
    // Show context menu
    showMenu(
      context: context,
      position: RelativeRect.fill,
      items: [
        PopupMenuItem(
          child: Text('Edit'),
          onTap: () => _editNode(),
        ),
        PopupMenuItem(
          child: Text('Delete'),
          onTap: () => controller.removeNode(node.id),
        ),
      ],
    );
  }
}

Selection States

Show visual feedback for selected nodes:

Widget buildNodeWidget(BuildContext context, Node<MyData> node) {
  final controller = NodeFlowController.of(context);
  final isSelected = controller.selectedNodeIds.contains(node.id);

  return Container(
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(8),
      border: Border.all(
        color: isSelected ? Colors.blue : Colors.grey[300]!,
        width: isSelected ? 3 : 1,
      ),
      boxShadow: isSelected
          ? [
              BoxShadow(
                color: Colors.blue.withOpacity(0.3),
                blurRadius: 12,
                spreadRadius: 2,
              ),
            ]
          : [
              BoxShadow(
                color: Colors.black12,
                blurRadius: 4,
                offset: Offset(0, 2),
              ),
            ],
    ),
    child: // ...node content
  );
}

Reactive Nodes with MobX

Since the package uses MobX, you can create reactive node widgets:

class ReactiveNodeWidget extends StatelessWidget {
  final Node<ObservableData> node;

  const ReactiveNodeWidget({required this.node});

  @override
  Widget build(BuildContext context) {
    return Observer(
      builder: (_) => Container(
        decoration: BoxDecoration(
          color: node.data.color.value,
          borderRadius: BorderRadius.circular(8),
          border: Border.all(
            color: node.data.isActive.value
                ? Colors.green
                : Colors.grey,
          ),
        ),
        child: Column(
          children: [
            Text(node.data.title.value),
            if (node.data.isProcessing.value)
              CircularProgressIndicator(),
          ],
        ),
      ),
    );
  }
}

Custom Shapes

Create nodes with custom shapes:

class CircularNodeWidget extends StatelessWidget {
  final Node<MyData> node;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: node.size.width,
      height: node.size.height,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.purple[50],
        border: Border.all(color: Colors.purple, width: 2),
      ),
      child: Center(child: Text(node.data.label)),
    );
  }
}

class HexagonNodeWidget extends StatelessWidget {
  final Node<MyData> node;

  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: HexagonClipper(),
      child: Container(
        width: node.size.width,
        height: node.size.height,
        color: Colors.teal[50],
        child: Center(child: Text(node.data.label)),
      ),
    );
  }
}

Performance Optimization

Keep node widgets performant:

1. Use const constructors

class StaticNodeWidget extends StatelessWidget {
  final String title;

  const StaticNodeWidget({required this.title});

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(title),
    );
  }
}

2. Minimize rebuilds

nodeBuilder: (context, node) {
  // Only rebuild when node data changes
  return NodeWidget(
    key: ValueKey(node.id),
    node: node,
  );
}

3. Avoid expensive operations

// ❌ Bad: Expensive operation in build
Widget build(BuildContext context) {
  final processedData = expensiveComputation(node.data); // Runs every build!
  return Text(processedData);
}

// ✅ Good: Cache expensive operations
class NodeWidget extends StatefulWidget {
  @override
  State<NodeWidget> createState() => _NodeWidgetState();
}

class _NodeWidgetState extends State<NodeWidget> {
  late String processedData;

  @override
  void initState() {
    super.initState();
    processedData = expensiveComputation(widget.node.data);
  }

  @override
  Widget build(BuildContext context) {
    return Text(processedData);
  }
}

Best Practices

  1. Fixed Sizes: Always respect node.size.width and node.size.height
  2. Overflow Handling: Use overflow: TextOverflow.ellipsis for long text
  3. Accessibility: Add semantic labels for screen readers
  4. Consistent Styling: Maintain visual consistency across node types
  5. Loading States: Show indicators for async operations
  6. Error States: Display error messages within the node
  7. Touch Targets: Ensure interactive elements are at least 44x44 pixels

Common Patterns

Node with Status Badge

Widget buildNodeWithBadge(Node<MyData> node) {
  return Stack(
    clipBehavior: Clip.none,
    children: [
      Container(
        // Main node content
      ),
      Positioned(
        top: -8,
        right: -8,
        child: Container(
          padding: EdgeInsets.all(4),
          decoration: BoxDecoration(
            color: Colors.red,
            shape: BoxShape.circle,
          ),
          child: Text(
            node.data.errorCount.toString(),
            style: TextStyle(color: Colors.white, fontSize: 10),
          ),
        ),
      ),
    ],
  );
}

Node with Progress Bar

Widget buildNodeWithProgress(Node<ProcessData> node) {
  return Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      Container(
        // Node content
      ),
      LinearProgressIndicator(
        value: node.data.progress,
        backgroundColor: Colors.grey[200],
        color: Colors.blue,
      ),
    ],
  );
}

See Also

On this page