This feels like a basic problem and I somehow made it work, but I'm not happy with my solution. So I hope someone can jump in and suggest a more elegant one.
I have a column-based layout derived from the flutter_simple_treeview example. The google official repo has the full source code, and a web demo.
My goal is to modify the layout so that
This is what I've got. The TreeView and the bottom buttons are wrapped in colored containers only for clarity.
The page in the above screenshot is rendered with the code below:
lib/trees/controller_usage.dart
import 'package:flutter/material.dart';
import 'package:flutter_simple_treeview/flutter_simple_treeview.dart';
class ControllerUsage extends StatefulWidget {
@override
_ControllerUsageState createState() => _ControllerUsageState();
}
class _ControllerUsageState extends State<ControllerUsage> {
final Key _key = ValueKey(22);
final TreeController _controller = TreeController(allNodesExpanded: true);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 530,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
flex: 2,
child: SizedBox( // tight constraints
// height: 450,
width: 350,
child: buildTree(),
),
),
//TODO:
// - place a row of btns at the bottom
// const Spacer(),
Container(
color: Colors.amber,
child: Row(
children: [
ElevatedButton(
child: const Text("Unfold All"),
onPressed: () => setState(() {
_controller.expandAll();
}),
),
ElevatedButton(
child: const Text("Fold All"),
onPressed: () => setState(() {
_controller.collapseAll();
}),
),
ElevatedButton(
child: const Text("Unfold 22"),
onPressed: () => setState(() {
_controller.expandNode(_key);
}),
),
ElevatedButton(
child: const Text("Fold 22"),
onPressed: () => setState(() {
_controller.collapseNode(_key);
}),
),
],
),
),
],
),
);
}
Widget buildTree() {
return Expanded(
child: Container(
color: Colors.blueAccent,
child: TreeView(
treeController: _controller,
nodes: [
TreeNode(content: const Text("node 11")),
TreeNode(
content: const Icon(Icons.audiotrack),
children: [
TreeNode(content: const Text("node 21")),
TreeNode(
content: const Text("node 22"),
key: _key,
children: [
TreeNode(
content: const Icon(Icons.sentiment_very_satisfied),
),
],
),
TreeNode(
content: const Text("node 23"),
),
],
),
],
),
),
);
}
}
lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:url_launcher/url_launcher.dart';
import 'trees/controller_usage.dart';
import 'trees/tree_from_json.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('flutter_simple_treeview Demo'),
actions: [
TextButton(
child: const Text(
"Source Code",
style: TextStyle(color: Colors.white),
),
onPressed: () async => await launch(
'https://github.com/google/flutter.widgets/tree/master/packages/flutter_simple_treeview/example')),
],
bottom: const TabBar(
tabs: [
Tab(text: "Tree Controller Usage"),
Tab(text: "Tree From JSON"),
],
),
),
body: TabBarView(
children: [
buildBodyFrame(ControllerUsage()),
buildBodyFrame(TreeFromJson()),
],
),
),
),
);
}
/// Adds scrolling and padding to the [content].
Widget buildBodyFrame(Widget content) {
return Container(
color: Colors.green,
child: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.all(10),
child: content,
),
),
),
);
}
}
To have the widgets fill the screen, I ended up using a SizedBox
inside the SingleChildScrollView
as a constraint. Other failed attempts:
SizedBox
with the same height constraint around the SingleChildScroolView
gives me a blank screen.SizedBox
inside the SingleChildScroolView
without a height constraint gives me a blank screen.Expanded
inside the SingleChildScroolView
instead of the SizedBox
gives me a blank screen as well.Then I got a bad feeling about that magic nunber height constraint because then I'd have to adapt to different screens this way. I wish I could simply "expand" to the screen size without magic numbers or MediaQuery
. Is this possible?
To make the TreeView
"push" the button Row to the bottom, I had to use a Flexible
around the TreeView
. This feels goofy as well because it seems to break the code symmetry.
Another problem I found with the SingleChildScrollView
:
As I add more TreeNode
s to the TreeView
, I expect that the scroll can work automatically. But in reality, I get the offshoot errors as shown below.
I'd appreciate it if someone can suggest an improvement.
In buildBodyFrame
, remove SingleChildScrollView
and add it to buildTree
then use LayoutBuilder
, to calculate remain size in column for tree.
App class:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 1,
child: Scaffold(
appBar: AppBar(
title: const Text('flutter_simple_treeview Demo'),
actions: [
TextButton(
child: const Text(
"Source Code",
style: TextStyle(color: Colors.white),
),
onPressed: () async {}),
],
bottom: const TabBar(
tabs: [
Tab(text: "Tree Controller Usage"),
// Tab(text: "Tree From JSON"),
],
),
),
body: TabBarView(
children: [
buildBodyFrame(ControllerUsage()),
// buildBodyFrame(TreeFromJson()),
],
),
),
),
);
}
/// Adds scrolling and padding to the [content].
Widget buildBodyFrame(Widget content) {
return Container(
padding: const EdgeInsets.all(10),
color: Colors.green,
child: content,
);
}
}
ControllerUsage class :
class ControllerUsage extends StatefulWidget {
@override
_ControllerUsageState createState() => _ControllerUsageState();
}
class _ControllerUsageState extends State<ControllerUsage> {
final Key _key = ValueKey(22);
final TreeController _controller = TreeController(allNodesExpanded: true);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(child: LayoutBuilder(
builder: (context, constraints) {
return Container(
height: constraints.maxHeight,
width: double.infinity,
child: buildTree());
},
)),
Container(
color: Colors.amber,
child: Row(
children: [
ElevatedButton(
child: const Text("Unfold All"),
onPressed: () => setState(() {
_controller.expandAll();
}),
),
ElevatedButton(
child: const Text("Fold All"),
onPressed: () => setState(() {
_controller.collapseAll();
}),
),
ElevatedButton(
child: const Text("Unfold 22"),
onPressed: () => setState(() {
_controller.expandNode(_key);
}),
),
ElevatedButton(
child: const Text("Fold 22"),
onPressed: () => setState(() {
_controller.collapseNode(_key);
}),
),
],
),
),
],
);
}
Widget buildTree() {
return SingleChildScrollView(
child: Container(
color: Colors.blueAccent,
child: TreeView(
treeController: _controller,
nodes: [
TreeNode(content: const Text("node 11")),
TreeNode(
content: const Icon(Icons.audiotrack),
children: [
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(content: const Text("node 21")),
TreeNode(
content: const Text("node 22"),
key: _key,
children: [
TreeNode(
content: const Icon(Icons.sentiment_very_satisfied),
),
],
),
TreeNode(
content: const Text("node 23"),
),
],
),
],
),
),
);
}
}