Flutter – Portrait and Landscape Layouts

In this article we will explore how to build a Flutter form that can display itself beautifully whether in a portrait or landscape orientation. If you've ever built a Flutter form you've probably found it to be pretty easy to support a portrait orientation. But rotate the phone sideways and oops! - things don't look quite as nice.

For our app we will also go a bit further than your basic run-of-the-mill form and show you how to do a few other things such as:

  • Adding a full screen background image
  • Displaying a logo
  • Changing the theme of the standard form

Here is what the finished app will look like in portrait orientation:
App Image Portrait

And in landscape orientation:
App Image Landscape

Building the App

In this Flutter app we are going to start off by modifying the build method. Create a new Flutter app and modify the build method so that we can call a body() method:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: body(context)
    );
  }

For the body() method, it is also short and sweet - basically it will call either the portrait() or landscape() method. Which method is called is dependent on the current device orientation:

  Widget body(BuildContext context) {
    if(MediaQuery.of(context).orientation == Orientation.portrait)
    {
      return portrait();
    }
    else {
      return landscape();
    }
  }

The Bits and Pieces...

Before we go into showing you the portrait and landscape methods, a discussion of my approach to building our login form is warranted.

Break the Form into Smaller Pieces

Firstly we are going to break everything out into smaller methods to make it easier to understand and reuse code between the portrait and landscape layouts. This means that if you want to see how the username and password fields are put together, you can refer to the corresponding methods. For example here is how we define the username form field:

  TextFormField username() {
    return TextFormField(
      decoration: const InputDecoration(
        hintText: 'Username',
        labelText: 'Username',
        border: OutlineInputBorder(
            borderRadius: BorderRadius.all(Radius.circular(8))),
      ),
    );
  }

Apply a Theme

For this app, I did not create a custom Theme or greatly modify the default one. As a matter of fact, I just modified the primarySwatch to Orange (it defaults to blue and there is no real reason I had to do this other than for fun). I wanted to avoid creating a new Theme for the entire app because I understood that the theme of the login form may be very much different than the rest of the app. For example I chose white as the primary color for the login form. But what if the rest of the app that you see after login has a white background - that would be bad. On the other hand if you want all of the form fields in your app to have the same color, feel free to move the theming of just the login form up to the MaterialApp.theme and have fun with it 🙂

Here is a basic Theme we create while building our layout. We are careful to insert it into the tree of widgets so that it impacts only our form:

Theme(
    data: Theme.of(context).copyWith(
        //make labels and border white when not focused
        hintColor: Colors.white,
        textTheme: TextTheme(
            //make text value white
            body1: TextStyle(color: Colors.white)),
    ),
    child: Container(
        //Form is inside this container
    ),
),

Arrange the Layout

Honestly this was the more difficult part of the app to get just right. It really required the right combination of layout widgets to make it work as expected. You'll see that I predominantly relied on Columns and Rows and made sure each widget expands as expected to take up as much as possible of the width of the screen. For example:

child: Column(
    children: <Widget>[
        Expanded(
            flex: 2,
            child: Container(
                child: logo(),
                margin: EdgeInsets.symmetric(vertical: 30),
                alignment: Alignment.bottomCenter,
            ),
        ),
...

Landscape Orientation Required More Adjustments

When an app is displaying in lanscape orientation there is much less height available. This makes it impossible to vertically list all of our widgets as there is just not enough space for all of this:

  • logo
  • username field
  • password field
  • login button
  • signUp button

We might have resorted to using a ListView, but if you have ever had to scroll on your phone while you filled out a form, you will remember it as clunky. So what I did to solve this space issue was:

  1. Allow the logo to slide behind the form fields if the keyboard is opened. This was achieved by using the Stack Widget.
  2. Put the username and password fields on the same line.

These two changes were enough to allow the form to look nice in landscape mode even with the keyboard open:
App Image Landscape Keyboard

Code for Portrait Layout

The portrait layout was achieved by using a single column arrangement. You'll also notice we gave the logo a little more of a flex so that it gets more space than the other widgets.

  Widget portrait() {
    return new Container(
      decoration: new BoxDecoration(
        image: new DecorationImage(
          image: new ExactAssetImage('assets/background.jpg'),
          fit: BoxFit.cover,
        ),
      ),
      child: Theme(
        data: Theme.of(context).copyWith(
          //make labels and border white when not focused
          hintColor: Colors.white,
          textTheme: TextTheme(
              //make text value white
              body1: TextStyle(color: Colors.white)),
        ),
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 30),
          child: Column(
            children: <Widget>[
              Expanded(
                flex: 2,
                child: Container(
                  child: logo(),
                  margin: EdgeInsets.symmetric(vertical: 30),
                  alignment: Alignment.bottomCenter,
                ),
              ),
              Expanded(
                child: Container(
                  child: username(),
                  alignment: Alignment.bottomCenter,
                ),
                flex: 1,
              ),
              Expanded(
                child: Container(
                  child: password(),
                  margin: EdgeInsets.only(top: 12),
                  alignment: Alignment.topCenter,
                ),
                flex: 1,
              ),
              Expanded(
                child: Container(
                  child: loginButton(),
                  alignment: Alignment.topCenter,
                ),
                flex: 1,
              ),
              Expanded(
                child: Container(
                  child: signUpButton(),
                  alignment: Alignment.bottomCenter,
                ),
                flex: 1,
              ),
            ],
          ),
        ),
      ),
    );
  }

Code for Landscape Layout

The landscape layout was achieved by a single column with multiple rows. As described earlier, we have both the username and password fields on a single row. What else is special about this layout is the usage of the [Stack widget](). This widget allows you to place widgets on top of one another. The first child is at the bottom and so on. This is what allows the logo and the form to essentially 'float' on top of the background when the keyboard is opened and pushes the controls up.

  Widget landscape() {
    return new Stack(
      children: <Widget>[
        Container(
          decoration: new BoxDecoration(
            image: new DecorationImage(
              image: new ExactAssetImage('assets/background.jpg'),
              fit: BoxFit.cover,
            ),
          ),
        ),
        Container(
          alignment: Alignment.topCenter,
          margin: EdgeInsets.only(top: 80),
          child: logo(),
        ),
        Theme(
          data: Theme.of(context).copyWith(
            //make labels and border white when not focused
            hintColor: Colors.white,
            textTheme: TextTheme(
                //make text value white
                body1: TextStyle(color: Colors.white)),
          ),
          child: Container(
            padding: EdgeInsets.only(left: 10, right: 10, top: 30),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.end,
              children: <Widget>[
                Row(
                  children: <Widget>[
                    Expanded(
                      child: Container(
                        child: username(),
                        margin: EdgeInsets.symmetric(horizontal: 5),
                      ),
                    ),
                    Expanded(
                      child: Container(
                        child: password(),
                        margin: EdgeInsets.symmetric(horizontal: 5),
                      ),
                    ),
                  ],
                ),
                Row(
                  children: <Widget>[
                    Expanded(
                      child: Container(
                        child: loginButton(),
                        margin: EdgeInsets.symmetric(horizontal: 5),
                      ),
                    ),
                  ],
                ),
                Row(
                  children: <Widget>[
                    Expanded(
                      child: Container(
                        child: signUpButton(),
                        margin: EdgeInsets.symmetric(horizontal: 5),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

That's a Wrap!

There are lots of options when it comes to laying out a screen in Flutter. It is important to consider both portrait and landscape orientations and test each with and without the keyboard open. It's also a good idea to become familiar with all of the different [Layout Widgets] (https://flutter.dev/docs/development/ui/widgets/layout) and know which is the most appropriate for the job. Hopefully in this article you've seen a pretty good example of how flexible Flutter can be in accomodating your various layout needs.

-Joe

4 thoughts on “Flutter – Portrait and Landscape Layouts”

  1. Respected sir,
    Thanks for publish beautiful article, I need for some help like ” one dropdown button depend on second dropdown button” .
    E.g – I select state then another dropdown button show only this district under first selected state. Please reply mi on my mail

  2. I observed you didn’t use the stateful widget on the form… Is there a reason for that?

    Or better still… What’s the right order of code for using the stateful widget with the portrait or landscape widget?

    1. Hi there,
      If you inspect the code you will see that the page containing the login form is indeed a StatefulWidget. Do you perhaps mean why did I not use a Form widget? Form widgets are optional and I only left it out of my example to keep things simple. But I do encourage their usage in most cases. For example they are beneficial if you want to look at the entire form in regards to validation. If you were to bring in a form widget, it would likely go into the widget tree just below the Theme.

Leave a Reply

Your email address will not be published. Required fields are marked *