Flutter Tutorials

Building Flutter Find Widget in Tree

·
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.

Table of Contents

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](https://api.flutter.dev/flutter/widgets/Transform-class.html).

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.

Ambika Dulal

About Ambika Dulal

Lead Mobile App Developer and Tech Consultant specializing in Flutter, Dart, and Firebase.