Flutter Widget to show a nested tree structure?
Asked Answered
L

6

10

We have an N level (max is probably around 10 or so) nested data structure that basically resembles a folder layout ..

Each node at any level is a Mime type of something to show or a URL ..

My question is actually very simple .. is there any available Fluter Widget that can show this type of stucture -- allowing the common "open/close" at any parent level, etc. ??

This seems like a pretty fundamental UI element not be had in the stock toolbox but I haven't had any luck finding one ..

TIA!

/Steve

Labionasal answered 9/10, 2018 at 17:31 Comment(3)
Were you able to implement tree in Flutter? If yes, then please guide me.Danette
@ZainSMJ - Not precisely as desired but I did actually get a decent result by using a Listview with ExpansionTile as the children widgets. Not as fancy as a tree but it does work very well ..Labionasal
LoL. I have done the same :)Danette
T
6

Over the past few weeks I have been working on a TreeView widget and have come up with a basic structure. It is now available in the pub for use. Working on it is easy enough once you really know how to do it. I've got to admit that documentation has never been my strong point, but if someone has any trouble with this just add an issue on the Github page.

Any suggestions to improve the project are also welcome.

Sample Code

Let's assume this is the directory structure we want to implement using the TreeView widget

Desktop
|-- documents
|   |-- Resume.docx
|   |-- Billing-Info.docx
|-- MeetingReport.xls
|-- MeetingReport.pdf
|-- Demo.zip

In this example

  1. Resume.docx and Billing-Info.docx are Child widgets with documents as the Parent.
  2. documents, MeetingReport.xls, MeetingReport.xls and Demo.zip are Child widgets with Desktop as a Parent widget.

var treeView = TreeView(
  parentList: [
    Parent(
      parent: Text('Desktop'),
      childList: ChildList(
        children: [
          Parent(
            parent: Text('documents'),
            childList: ChildList(
              children: [
                Text('Resume.docx'),
                Text('Billing-Info.docx'),
              ],
            ),
          ),
          Text('MeetingReport.xls'),
          Text('MeetingReport.pdf'),
          Text('Demo.zip'),
        ],
      ),
    ),
  ],
);

This will not generate anything fancy at all. But instead of all the Text widgets you can pass any complex widget and it will still work.

A screen grab of an application using TreeView

Therontheropod answered 14/1, 2019 at 12:34 Comment(8)
This looks pretty promising -- once I am at a decent pausing state I will take a look. I like how the layout is generated .. although until I try it with dynamic data from a DB source I'll reserve judgement but I think it should work pretty good. Is there a practical limitation to the level of nesting? What happens to the indentation when 5 or 6 levels in?Labionasal
The tree view by default does not add any indentation. That has to be implemented seperately. I have an example in the GitHub repo. But it is not well documented as of nowTherontheropod
And I have not tested this extensively eitherTherontheropod
Any progress on this?Turro
@Turro I haven't pushed any changes since I originally published. Try the existing version, it should fit some use casesTherontheropod
This looks good. but if a leaf node is several times deep then leaf node contains a text then there wont be enough space to display text due to indentationSacred
@DilipAgheda True. But the intention is to let the user decide how to implement that. The Card you see in the gif above is not part of the TreeView library as such. I have left it up to the user to decide on the indentation, etc.Therontheropod
Thanks @AjilO. If I want to display 5 level deep tree where leaf node is a text of upto 5 lines. I am not able to get visually pleasing results. what do you suggest best way to handle this for better UX? you may not be UX person but I only need your opinion.Sacred
V
3

Bit late to the party.I have also been searching for a treeview structure to use for my flutter project but i couldn't find any that suits my needs.So, I created a package dynamic_treeview .It uses a parent/child relationship to build a treeview.See if this suits your needs.Let me know what you guys think.Thanks

Vulgarian answered 13/11, 2019 at 18:43 Comment(0)
M
1

Check flutter_simple_treeview. This is a demo.

Mauk answered 20/5, 2020 at 21:4 Comment(0)
G
1

Now there is a offical package in pub whitch support TreeView.

two_dimensional_scrollables

Garygarza answered 2/8 at 13:41 Comment(0)
G
0

There is a custom component doing what you need : Screenshot Preview

https://github.com/AndrewTran2018/flutter-piggy-treeview

Gardenia answered 18/10, 2018 at 8:51 Comment(1)
Doesn't work really much at all -- not Dart 2 compliant -- and lots of interdependencies .. but yes.. it was a good launching point..Labionasal
E
0

Here is an example Tree View

The approach here is that you build your tree with a hierarchical TreeNode structure. Each node has a ValueNotifier to let the tree know when isExpanded changes. The tree's state exposes expandAll() and collapseAll() via GlobalKey so you can recursively collapse/expand.

import 'package:arborio/expander.dart';
import 'package:flutter/material.dart';

///[GlobalKey] for controlling the state of the [TreeView]
class TreeViewKey<T> extends GlobalKey<_TreeViewState<T>> {
  ///Creates a [GlobalKey] for controlling the state of the [TreeView]
  const TreeViewKey() : super.constructor();
}

///The callback function for building tree nodes, including animation values
typedef TreeViewBuilder<T> = Widget Function(
  BuildContext context,
  TreeNode<T> node,
  bool isSelected,
  Animation<double> expansionAnimation,
  void Function(TreeNode<T> node) select,
);

///The callback function when the node expands or collapses
typedef ExpansionChanged<T> = void Function(TreeNode<T> node, bool expanded);

void _defaultExpansionChanged<T>(TreeNode<T> node, bool expanded) {}
void _defaultSelectionChanged<T>(TreeNode<T> node) {}

///Represents a tree node in the [TreeView]
class TreeNode<T> {
  ///Creates a tree node
  TreeNode(
    this.key,
    this.data, [
    List<TreeNode<T>>? children,
    bool isExpanded = false,
  ])  : children = children ?? <TreeNode<T>>[],
        isExpanded = ValueNotifier(isExpanded);

  ///The unique key for this node
  final Key key;

  ///The data for this node
  final T data;

  ///The children of this node
  final List<TreeNode<T>> children;

  ///Whether or not this node is expanded. Changing this value will cause the
  ///node's expander to animate open or closed
  ValueNotifier<bool> isExpanded;
}

///A tree view widget that for displaying data hierarchically
class TreeView<T> extends StatefulWidget {
  ///Creates a [TreeView] widget
  const TreeView({
    required this.nodes,
    required this.builder,
    required this.expanderBuilder,
    ExpansionChanged<T>? onExpansionChanged,
    ValueChanged<TreeNode<T>>? onSelectionChanged,
    this.selectedNode,
    this.indentation = const SizedBox(width: 16),
    super.key,
    this.animationCurve = Curves.easeInOut,
    this.animationDuration = const Duration(milliseconds: 500),
  })  : onExpansionChanged = onExpansionChanged ?? _defaultExpansionChanged,
        onSelectionChanged = onSelectionChanged ?? _defaultSelectionChanged;

  ///The root nodes for this tree view, which can have children
  final List<TreeNode<T>> nodes;

  ///Called when a node is expanded or collapsed
  final ExpansionChanged<T> onExpansionChanged;

  ///Called when the selected node changes
  final ValueChanged<TreeNode<T>> onSelectionChanged;

  ///The currently selected node
  final TreeNode<T>? selectedNode;

  ///The widget to use for indentation of nodes
  final Widget indentation;

  ///The builder for the expander icon (usually an arrow icon or similar)
  final ExpanderBuilder expanderBuilder;

  ///The builder for the content of the expander (usually icon and text)
  final TreeViewBuilder<T> builder;

  ///This modulates the animation for the expander when it opens and closes
  final Curve animationCurve;

  ///The duration of the animation for the expander when it opens and closes
  final Duration animationDuration;

  @override
  State<TreeView<T>> createState() => _TreeViewState<T>();
}

class _TreeViewState<T> extends State<TreeView<T>> {
  TreeNode<T>? _selectedNode;

  @override
  void initState() {
    super.initState();
    _selectedNode = widget.selectedNode;
    widget.nodes.forEach(_listen);
  }

  void _listen(TreeNode<T> node) {
    node.children.forEach(_listen);
    node.isExpanded.addListener(() => setState(() {}));
  }

  void collapseAll() => setState(() {
        for (final node in widget.nodes) {
          _setIsExpanded(node, false);
        }
      });

  void expandAll() => setState(() {
        for (final node in widget.nodes) {
          _setIsExpanded(node, true);
        }
      });

  void _setIsExpanded(TreeNode<T> node, bool isExpanded) {
    for (final n in node.children) {
      _setIsExpanded(n, isExpanded);
    }

    node.isExpanded.value = isExpanded;
  }

  void _handleSelection(TreeNode<T> node) {
    setState(() {
      _selectedNode = node;
    });
    widget.onSelectionChanged(node);
  }

  @override
  Widget build(BuildContext context) => ListView(
        children: widget.nodes
            .map((node) => _buildNode(node, widget.onExpansionChanged))
            .toList(),
      );

  Widget _buildNode(
    TreeNode<T> node,
    ExpansionChanged<T> expansionChanged,
  ) =>
      Theme(
        data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
        child: Row(
          children: [
            widget.indentation,
            Expanded(
              child: Expander<T>(
                animationDuration: widget.animationDuration,
                animationCurve: widget.animationCurve,
                expanderBuilder: widget.expanderBuilder,
                canExpand: node.children.isNotEmpty,
                key: PageStorageKey<Key>(node.key),
                contentBuilder: (context, isExpanded, animationValue) =>
                    widget.builder(
                  context,
                  node,
                  _selectedNode?.key == node.key,
                  animationValue,
                  _handleSelection,
                ),
                onExpansionChanged: (expanded) {
                  setState(() {
                    node.isExpanded.value = expanded;
                  });
                  expansionChanged(node, expanded);
                },
                isExpanded: node.isExpanded,
                children: node.children
                    .map((childNode) => _buildNode(childNode, expansionChanged))
                    .toList(),
              ),
            ),
          ],
        ),
      );

  @override
  void dispose() {
    for (final node in widget.nodes) {
      for (final childNode in node.children) {
        childNode.isExpanded.dispose();
      }
      node.isExpanded.dispose();
    }
    super.dispose();
  }
}

It uses an Expander, but an ExpansionTile is very similar. You can use an ExpansionTile here if you prefer.

Grab the package here.

See the live sample

Emogeneemollient answered 6/1 at 21:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.