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:



And in landscape orientation:



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:

Allow the logo to slide behind the form fields if the keyboard is opened. This was achieved by using the Stack Widget. 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:



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