Search code examples
flutterlayoutflutter-layouttreeview

flutter: Make a column layout expand to fill the screen


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.

Goal

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

  • The 4 buttons can lay as a row at the bottom of the screen.
  • The TreeView will fill the rest of the space.
  • No wasted screen space except for the padding.

The result

This is what I've got. The TreeView and the bottom buttons are wrapped in colored containers only for clarity.

enter image description here

The code

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,
          ),
        ),
      ),
    );
  }
}


Implementation details and questions

To have the widgets fill the screen, I ended up using a SizedBox inside the SingleChildScrollView as a constraint. Other failed attempts:

  • Placing the SizedBox with the same height constraint around the SingleChildScroolView gives me a blank screen.
  • Placing the SizedBox inside the SingleChildScroolView without a height constraint gives me a blank screen.
  • Placing an 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 TreeNodes to the TreeView, I expect that the scroll can work automatically. But in reality, I get the offshoot errors as shown below.

enter image description here

I'd appreciate it if someone can suggest an improvement.


Solution

  • 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"),
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      }
    }
    

    enter image description here