Solved: Flutter flexible layout, vertical overflow

Question

Asked by John on January 05, 2022 (source).

I'm starting out my first real world flutter app and so far I cannot wrap my head around how to solve this very basic use case I have.

I find various information online but none has really solved it yet. The use case seems so common to me that there has to a good standard way of doing this.

I have a column layout. To illustrate this there is a logo (blue), a form (red), a button (green) and some custom navigation at the bottom (black).

enter image description here

Here is the code:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.amber,
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(32.0),
          child: Column(
            children: [
              const Placeholder(
                color: Colors.blue,
                fallbackHeight: 100.0,
              ),
              const Padding(
                padding: EdgeInsets.symmetric(vertical: 32.0),
                child: Placeholder(
                  color: Colors.red,
                  fallbackHeight: 300.0,
                ),
              ),
              const Spacer(),
              Column(
                children: [
                  const Placeholder(
                    color: Colors.green,
                    fallbackHeight: 40.0,
                  ),
                  Container(
                    margin: const EdgeInsets.only(top: 40.0),
                    child: const Placeholder(
                      color: Colors.black,
                      fallbackHeight: 40.0,
                    ),
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

I'm running this on a Pixel 5 emulator. The sizes are not 100% accurate but good enough to explain the scenario I think.

This screen is part of a flow with similar screens, hence I want to layout the custom navigation and button from the bottom and up so to speak. I.e. I want the button and the nav to be at the same position when I navigate to the next screen.

The same is true with the logo and the main content from the top. Therefor I put a spacer in between that will take up the space in between.

To the issue. I have some validation within the form. When pressing the button and breaking the validation the form will display a bunch of messages below each field. I simulate this by increasing the height of the form placeholder to 500.

enter image description here

So now all the components won't fit in the screen anymore and I get some overflow at the bottom. Nothing strange. When researching the most idiomatic way to solve this I find it is to add a SingleChildScrollView which make sense.

But adding a SingleChildScrollView breaks the code since I have a Spacer in there. And I understand that part. Since we now say that "take up as much space as you want, I will make it scrollable" we have infinit of space at our disposal. At the same time the Spacer basically says, "I'll push everything down as much as I can until there are no space left". So this also make sense. Let's remove the spacer.

Here is the code:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.amber,
      body: SafeArea(
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(32.0),
            child: Column(
              children: [
                const Placeholder(
                  color: Colors.blue,
                  fallbackHeight: 100.0,
                ),
                const Padding(
                  padding: EdgeInsets.symmetric(vertical: 32.0),
                  child: Placeholder(
                    color: Colors.red,
                    fallbackHeight: 500.0,
                  ),
                ),
                Column(
                  children: [
                    const Placeholder(
                      color: Colors.green,
                      fallbackHeight: 40.0,
                    ),
                    Container(
                      margin: const EdgeInsets.only(top: 40.0),
                      child: const Placeholder(
                        color: Colors.black,
                        fallbackHeight: 40.0,
                      ),
                    ),
                  ],
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

It works as expected. I get no error and I can scroll to see the button and navigation.

Now to the sum this up. What happens when my form goes back to it's original height, 300px? Of course since the spacer is removed I don't have that behaviour I want anymore where the button and nav is based from the bottom. They are now pulled up to the form instead. And of course, this also make sense.

enter image description here

I understand why all the different scenarios act as they do. But how can I then create a flexible layout where some stuff are positioned based from the bottom and some from the top but still protect against the overflow?

Do I have to start calculating when and if any overflow is going to occur and dynamically add a scroll view? I haven't gone down that route yet because I was hoping there was a more declarative, standard way of dealing with this.

Answer

Question answered by Wouter (source).

This should do the trick:

return Scaffold(
      appBar: AppBar(
        title: Text('Demo'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Expanded(
              child: ListView(children: [
                Text('Logo'),
               SizedBox(height: 1000,), //large placeholder
                Text('bottom')
              ],)),
          ElevatedButton(onPressed: () {}, child: Text('Your button'))
        ],
      ),
    );

So basically you start filling up your screen from the bottom with your button and then with the expanded widget you fill up the rest of the available space, wchich you fill up with a scrollable widget like ListView (which is better than SingleChildscrollView for more than one widget as it's child.

To apply this concept to you code:

return Scaffold(
      backgroundColor: Colors.amber,
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            Expanded(
              child: SingleChildScrollView(
                child: Padding(
                  padding: const EdgeInsets.all(32.0),
                  child: Column(
                    children: [
                      const Placeholder(
                        color: Colors.blue,
                        fallbackHeight: 100.0,
                      ),
                      const Padding(
                        padding: EdgeInsets.symmetric(vertical: 32.0),
                        child: Placeholder(
                          color: Colors.red,
                          fallbackHeight: 500.0,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            Column(
              children: [
                const Placeholder(
                  color: Colors.green,
                  fallbackHeight: 40.0,
                ),
                Container(
                  margin: const EdgeInsets.only(top: 40.0),
                  child: const Placeholder(
                    color: Colors.black,
                    fallbackHeight: 40.0,
                  ),
                ),
              ],
            )
          ],
        ),
      ),
    );
FLUTTER FLUTTER-LAYOUT
SHARE: