Building Flutter Find Widget in Tree

In this tutorial, we’ll explore how to create a hierarchical node structure in Flutter or Flutter Find Widget in Tree using widgets and manage interactions like scaling, dragging, and adding child nodes.

Let’s dive into the code and understand how everything works together.

Building Flutter Find Widget in Tree

Overview of Flutter Find Widget in Tree

We’ll build a Flutter application that visualizes hierarchical nodes. Each node can have children nodes, which can be expanded or collapsed. Users can also add new child nodes dynamically.

Setting Up the App -Flutter Find Widget in Tree

First, we set up a basic Flutter application with a MaterialApp and a Scaffold that hosts our hierarchical nodes view.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Hierarchical Nodes',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HierarchicalView(),
    );
  }
}

HierarchicalView Widget -Flutter Find Widget in Tree

Our main view is HierarchicalView, a stateful widget that manages scaling, dragging, and holds the root node of our hierarchy.

class HierarchicalView extends StatefulWidget {
  const HierarchicalView({Key? key}) : super(key: key);

  @override
  _HierarchicalViewState createState() => _HierarchicalViewState();
}

class _HierarchicalViewState extends State<HierarchicalView> {
  double _scale = 1.0;
  double _previousScale = 1.0;
  Offset _offset = Offset.zero;
  Offset _normalizedOffset = Offset.zero;

  final GlobalKey _parentKey = GlobalKey();
  final Map<GlobalKey, Offset> _nodePositions = {};
  final Map<NodeData, bool> _expandedNodes = {};

  final NodeData _parentNode = NodeData(
    key: GlobalKey(),
    label: 'Parent Node',
    children: [
      NodeData(
        key: GlobalKey(),
        label: 'Child 1',
      ),
      NodeData(
        key: GlobalKey(),
        label: 'Child 2',
      ),
    ],
  );

  @override
  void initState() {
    super.initState();
    _initializeExpandedNodes(_parentNode);
  }

  void _initializeExpandedNodes(NodeData node) {
    _expandedNodes[node] = true;
    for (var child in node.children) {
      _initializeExpandedNodes(child);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hierarchical Nodes'),
      ),
      body: Column(
        children: [
          Expanded(
            child: GestureDetector(
              onScaleStart: (ScaleStartDetails details) {
                _previousScale = _scale;
                _normalizedOffset = (_offset - details.focalPoint) / _scale;
                setState(() {});
              },
              onScaleUpdate: (ScaleUpdateDetails details) {
                _scale = _previousScale * details.scale;
                _offset = details.focalPoint + _normalizedOffset * _scale;
                setState(() {});
              },
              onScaleEnd: (ScaleEndDetails details) {
                _previousScale = 1.0;
              },
              child: SingleChildScrollView(
                child: Container(
                  height: MediaQuery.of(context).size.height,
                  width: MediaQuery.of(context).size.width,
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [Colors.blue.shade200, Colors.blue.shade900],
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                    ),
                  ),
                  child: Transform(
                    transform: Matrix4.identity()
                      ..translate(_offset.dx, _offset.dy)
                      ..scale(_scale),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.start,
                      children: [
                        Node(
                          key: _parentKey,
                          data: _parentNode,
                          nodePositions: _nodePositions,
                          expandedNodes: _expandedNodes,
                          onAddChild: _addChild,
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _addChild(NodeData parentNode, String childLabel) {
    setState(() {
      final newNode = NodeData(
        key: GlobalKey(),
        label: childLabel,
      );
      parentNode.addChild(newNode);
      _expandedNodes[newNode] = true; // Automatically expand new nodes
    });
  }
}

NodeData Class for Flutter Find Widget in Tree

Represents a node in our hierarchy. Each NodeData has a key, label, and potentially children nodes.

class NodeData {
  final GlobalKey key;
  final String label;
  final List<NodeData> children;

  NodeData({
    required this.key,
    required this.label,
    List<NodeData>? children,
  }) : children = children ?? [];

  void addChild(NodeData child) {
    children.add(child);
  }
}

Node Widget

A stateful widget representing a node in the UI. It manages its expansion state and position updates.

class Node extends StatefulWidget {
  final NodeData data;
  final Map<GlobalKey, Offset> nodePositions;
  final Map<NodeData, bool> expandedNodes;
  final void Function(NodeData parentNode, String childLabel) onAddChild;

  const Node({
    Key? key,
    required this.data,
    required this.nodePositions,
    required this.expandedNodes,
    required this.onAddChild,
  }) : super(key: key);

  @override
  _NodeState createState() => _NodeState();
}

class _NodeState extends State<Node> {
  bool _expanded = false;
  late final GlobalKey _key;

  @override
  void didUpdateWidget(Node oldWidget) {
    super.didUpdateWidget(oldWidget);
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      _updatePosition();
    });
  }

  @override
  void initState() {
    super.initState();
    _key = widget.data.key;
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      _updatePosition();
    });
    _expanded = widget.expandedNodes[widget.data]!;
  }

  void _updatePosition() {
    final RenderBox? renderBox =
        _key.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox != null) {
      final Offset position = renderBox.localToGlobal(Offset.zero);
      widget.nodePositions[_key] = position;
    }
  }

  void _showAddChildDialog() {
    final TextEditingController childNameController = TextEditingController();

    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Add Child Node'),
          content: TextField(
            controller: childNameController,
            decoration: const InputDecoration(
              labelText: 'Child Name',
            ),
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                if (childNameController.text.isNotEmpty) {
                  widget.onAddChild(
                    widget.data,
                    childNameController.text,
                  );
                  Navigator.of(context).pop();
                }
              },
              child: const Text('Add'),
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        GestureDetector(
          onTap: () {
            setState(() {
              _expanded = !_expanded;
              widget.expandedNodes[widget.data] = _expanded;
            });
          },
          child: Container(
            key: ObjectKey(UniqueKey()),
            padding: const EdgeInsets.all(8.0),
            margin: const EdgeInsets.all(8.0),
            decoration: BoxDecoration(
              color: Colors.blueAccent,
              borderRadius: BorderRadius.circular(12),
              boxShadow: const [
                BoxShadow(
                  color: Colors.black26,
                  blurRadius: 6,
                  offset: Offset(0, 2),
                ),
              ],
            ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  widget.data.label,
                  style: const TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.person, color: Colors.white),
                  onPressed: _showAddChildDialog,
                ),
              ],
            ),
          ),
        ),
        if (_expanded && widget.data.children.isNotEmpty)
          Row(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: widget.data.children
                .map((child) => Node(
                      key: child.key,
                      data: child,
                      nodePositions: widget.nodePositions,
                      expandedNodes: widget.expandedNodes,
                      onAddChild: widget.onAddChild,
                    ))
                .toList(),
          ),
      ],
    );
  }
}

Also Read:

Conclusion

In this tutorial, we’ve covered how to build a hierarchical node structure in Flutter using widgets like StatefulWidget, GestureDetector, and Transform.

We managed scaling and dragging gestures, added dynamic child nodes, and visualized the hierarchy using CustomPaint and CustomPainter.

This setup allows for a flexible and interactive way to display and manage hierarchical data in your Flutter applications. You can expand on this foundation by adding animations, further customizations, or integrating with backend data sources for more dynamic content.

Visit GitHub for full code and demo output.

Share:
Ambika Dulal

Ambika Dulal is a Flutter Developer from Nepal who is passionate about building beautiful and user-friendly apps. She is always looking for new challenges and is eager to learn new things. She is also a strong believer in giving back to the community and is always willing to help others.

Leave a Comment

AO Logo

App Override is a leading mobile app development company based in Kathmandu, Nepal. Specializing in both Android and iOS app development, the company has established a strong reputation for delivering high-quality and innovative mobile solutions to clients across a range of industries.

Services

UI/UX Design

Custom App Development

Mobile Strategy Consulting

App Testing and Quality Assurance

App Maintenance and Support

App Marketing and Promotion

Contact

App Override

New Plaza, Kathmandu