I am working on a Flutter app where I need to generate a tree view from JSON data. The TreeView shown below:
The Table structured as follows:
ParentID | ChildID | Title |
---|---|---|
1 | 0 | Root 1 |
2 | 0 | Root 2 |
3 | 1 | Node 1.1 |
4 | 3 | Node 1.1.1 |
5 | 3 | Node 1.1.2 |
5 | 1 | Node 1.2 |
5 | 2 | Node 2.1 |
5 | 2 | Node 2.2 |
The JSON data is structured as follows:
[
{
"ParentID": 1,
"ChildID": 0,
"Title": "Root 1"
},
{
"ParentID": 2,
"ChildID": 0,
"Title": "Root 2"
},
{
"ParentID": 3,
"ChildID": 1,
"Title": "Node 1.1"
},
{
"ParentID": 4,
"ChildID": 3,
"Title": "Node 1.1.1"
},
{
"ParentID": 5,
"ChildID": 3,
"Title": "Node 1.1.2"
},
{
"ParentID": 6,
"ChildID": 1,
"Title": "Node 1.2"
},
{
"ParentID": 7,
"ChildID": 2,
"Title": "Node 2.1"
},
{
"ParentID": 8,
"ChildID": 2,
"Title": "Node 2.2"
}
]
This data represents a hierarchical structure where each node can have multiple children. The ParentID field indicates the parent node, and the ChildID field indicates the current node's position in the hierarchy. Currently, I have hardcoded data to generate the tree view, as shown below:
static const List<MyNode> roots = <MyNode>[
MyNode(
title: 'Root 1',
children: <MyNode>[
MyNode(
title: 'Node 1.1',
children: <MyNode>[
MyNode(title: 'Node 1.1.1'),
MyNode(title: 'Node 1.1.2'),
],
),
MyNode(title: 'Node 1.2'),
],
),
MyNode(
title: 'Root 2',
children: <MyNode>[
MyNode(
title: 'Node 2.1',
),
MyNode(title: 'Node 2.2')
],
),];
What I would like to achieve is to dynamically generate this tree view from the JSON data retrieved from a database. Could someone please help me convert this JSON data into a hierarchical tree view in Flutter? Any guidance or code examples would be greatly appreciated. Thank you in advance for your help!
You only need a single function, one that can "convert this JSON data into a hierarchical tree", like this extension function.
As for guidance, I would recommend to convert json data into an class (immutable if posible). I'm using here an extension but only because the type is somewhat narrow.
import 'package:flutter/material.dart';
void main() => runApp(const App());
final data = [
{"ParentID": 1, "ChildID": 0, "Title": "Root 1"},
{"ParentID": 2, "ChildID": 0, "Title": "Root 2"},
{"ParentID": 3, "ChildID": 1, "Title": "Node 1.1"},
{"ParentID": 4, "ChildID": 3, "Title": "Node 1.1.1"},
{"ParentID": 5, "ChildID": 3, "Title": "Node 1.1.2"},
{"ParentID": 6, "ChildID": 1, "Title": "Node 1.2"},
{"ParentID": 7, "ChildID": 2, "Title": "Node 2.1"},
{"ParentID": 8, "ChildID": 2, "Title": "Node 2.2"}
];
typedef NodeData = Map<String, Object>;
typedef NodePoint = MapEntry<NodeData, List<dynamic>?>;
extension NodeDataHelper on List<NodeData> {
// Maps the a list of [NodeData] into a list of
// [NodePoint] trees, nodes that don't have any
// children have a null as a value to indicate so.
List<NodePoint> as_tree_list() {
assert(isNotEmpty);
// Should you check if the data is malformed ?
// In recursion, the child node is the parent node
NodePoint _rec(NodeData node) {
final children = where(
(inode) => inode['ChildID'] == node['ParentID'],
);
return MapEntry(
node,
(children.isNotEmpty)
? children
.map(
(child_node) => _rec(child_node),
)
.toList(growable: false) // Does this need sorting ?
: null,
);
}
return where(
(inode) => inode['ChildID'] == 0,
).map(_rec).toList(growable: false);
}
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: data
.as_tree_list()
.map(
(tree) => TreeNode(node: tree),
)
.toList(growable: false),
),
),
);
}
}
class TreeNode extends StatelessWidget {
final NodePoint node;
const TreeNode({super.key, required this.node});
@override
Widget build(BuildContext context) {
Widget widget;
if (node.value != null) {
widget = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(node.key['Title'].toString()),
...node.value!.map(
(child) => TreeNode(node: child),
),
],
);
} else {
widget = Text(node.key['Title'].toString());
}
return Padding(
padding: const EdgeInsets.only(left: 25),
child: widget,
);
}
}
This example might seem a bit more convoluted but over all it's much better because converting json (a Map in this case) into a class removes all the lookups and thus making the code faster, but depending on how many fields the nodes have, the biggest benefit might be the type/value checks that class objects have (and you are getting lints from the lsp, the error are way more human readable).
import 'dart:convert';
import 'package:flutter/material.dart';
void main() => runApp(const App());
final data = [
{"ParentID": 1, "ChildID": 0, "Title": "Root 1"},
{"ParentID": 2, "ChildID": 0, "Title": "Root 2"},
{"ParentID": 3, "ChildID": 1, "Title": "Node 1.1"},
{"ParentID": 4, "ChildID": 3, "Title": "Node 1.1.1"},
{"ParentID": 5, "ChildID": 3, "Title": "Node 1.1.2"},
{"ParentID": 6, "ChildID": 1, "Title": "Node 1.2"},
{"ParentID": 7, "ChildID": 2, "Title": "Node 2.1"},
{"ParentID": 8, "ChildID": 2, "Title": "Node 2.2"}
];
typedef NodeData = Map<String, dynamic>;
class NodePoint {
final String title;
// Do you need this variables?
//final int child_id;
//final int parent_id;
final List<NodePoint> children;
const NodePoint._({
required this.title,
required this.children,
});
factory NodePoint.from_json(Map<String, dynamic> json_node, List<NodePoint> children) {
// Do your checks here if you need to, any throw during this call should be
// handled by the caller in order to drop bad nodes.
return NodePoint._(
title: json_node['Title'] as String,
children: children,
);
}
// Helper functions used while building the tree(s).
static int _parent_id(NodeData data) => data['ParentID'] as int;
static int _child_id(NodeData data) => data['ChildID'] as int;
// Maps the a list of [NodeData] into a list of [NodePoint] trees,
// nodes always contain a list of children that may be empty, use
// [has_children] to check if a node has children.
static List<NodePoint> nodes_from_json(String json) {
final data = jsonDecode(json);
assert(data is List, 'Not a list (${data.runtimeType}), wrong json?');
// The casting needs to happen in "layers" and you may want to
// type check all the variables like with the previous assert.
final nodes = (data as List).map((e) => (e as NodeData));
NodePoint _rec(NodeData node) => NodePoint.from_json(
node,
nodes
.where(
(inode) => _child_id(inode) == _parent_id(node),
)
.map(_rec)
.toList(growable: false));
// If the underlying functions are expected to throw,
// make the return null-able.
return nodes
.where(
(inode) => _child_id(inode) == 0,
)
.map(_rec)
.toList(growable: false);
}
// Just for convenience
bool get has_children => children.isNotEmpty;
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: NodePoint.nodes_from_json(jsonEncode(data))
.map(
(tree) => TreeNode(node: tree),
)
.toList(growable: false),
),
),
);
}
}
class TreeNode extends StatelessWidget {
final NodePoint node;
const TreeNode({super.key, required this.node});
@override
Widget build(BuildContext context) {
Widget widget;
if (node.has_children) {
widget = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(node.title),
...node.children.map(
(child) => TreeNode(node: child),
),
],
);
} else {
widget = Text(node.title);
}
return Padding(
padding: const EdgeInsets.only(left: 25),
child: widget,
);
}
}