Flutter Apps

10.2 Flutter Layout Widgets

Module Still Under Development

# Flutter Resources

Since everything in Flutter is created out of Widgets, it would make sense to become familiar with the most common ones.

The Flutter Widget Catalog is a great place to see what is available and learn more about them.

Every Flutter app that you build will have a Widget Tree and an Element Tree.

The Widget Tree is the total sum of all the Widget classes that you put together, each nested inside of another. It is a very similar concept to how you write HTML. Every HTML element has a parent all the way up to the root element - <html>. In Flutter, the root widget is the MaterialApp one that we use inside the first build method. Every widget has properties, just like every HTML element has attributes.

Every widget has a build method, which gets called to create an Element. The Elements are what actually renders on the screen. The Elements are arranged in a tree, in the same order as the widget tree.

So, every app you build will have a Widget tree and an Element tree.

There are lots of built-in widget classes that are part of Dart and Flutter. The material.dart package that we import provides many more. If you want to find more that have been created by the Flutter team as well as other developers, visit pub.dev. pub.dev is the official website to find Packages and Plugins that you can use in your Flutter App.

Here is a great reference for building your layouts - Flutter Layout Cheatsheet

Here is a YouTube Playlist on Widgets by the Google Developers channel.

# Page Structure

Your Flutter App will start with a handle of Widgets that will likely be the same for 90% of the apps that you build. Inside the build() method for your entry point we will add the following (possibly omitting some parts) as the return value.

return MaterialApp(
  home: Scaffold (
    appBar: AppBar(...),
    body: SafeArea(...),
    floatingActionButton: FloatingActionButton(...),
    bottomNavigationBar: BottomNavigationBar(...),
  )
)
1
2
3
4
5
6
7
8

The ...s will be replaced with the configuration properties.

Inside of the MaterialApp class we have the home property which contains the layout for your app. The Scaffold widget is usually the best choice for your layout. There are many other properties that you can add to the Scaffold. The code sample above shows a few common ones.

Later on, when we start adding navigation to multiple screens we will replace the home property with routes and initialRoute.

The appBar property loads an AppBar widget that will have, at least, a title: Text('Hello') property. It will be the standard bar at the top of your app for all the screens.

// example appBar
appBar: AppBar(
  title: const Text('BottomNavigationBar Sample'),
),
1
2
3
4

The AppBar also will commonly have a leading and an actions property. The leading property defines what you would place to the left of the title, usually a back button. The actions area is a space to the right of the title where you can add actions like a delete, add, and/or config button.

The body property loads the SafeArea widget which will contain all the content for the current screen. The SafeArea is a layout widget that checks for and avoids the notch that is at the top of many iPhone screens.

The floatingActionButton property is a FAB that you may or may not want. You are allowed to omit the property. It needs to have a child and an onPressed property.

// example FAB
floatingActionButton: FloatingActionButton(
  onPressed: () {
    // Add your onPressed code here!
  },
  backgroundColor: Colors.green,
  child: const Icon(Icons.navigation),
),
1
2
3
4
5
6
7
8

The bottomNavigationBar property has a configuration that includes an items property, whose value is a List of BottomNavigationBarItem widgets that you want in a navigation bar across the bottom of the screen. This is the navigation that stays constant across the app. If your app does not need this then you can omit the property.

//example bottomNavigationBar
bottomNavigationBar: BottomNavigationBar(
  items: const <BottomNavigationBarItem>[
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'Home',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.business),
      label: 'Business',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.school),
      label: 'School',
    ),
  ],
  currentIndex: _selectedIndex,
  selectedItemColor: Colors.amber[800],
  onTap: _onItemTapped,
),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

A List is a dart data structure similar to an Array or Set from JavaScript.

Now, there are other properties that you can add, and we will discuss many of them later on. Eg: the Drawer Widget for navigation can also be added to the Scaffold. For now, this is a good starting collection.

The Scaffold( body: SafeArea() ) is the part that will be repeated for every screen. So, each of your screens should have its own .dart file. All the screen files will go inside a /lib/screens folder. Back in your main.dart file, the MaterialApp widget will set the value of its home property to point to the Scaffold created in your home screen. The home screen file will be called something like home_screen.dart and it will have a Stateless Widget class called something like HomeScreen.

//inside main.dart
 Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen() //this is the class imported from our /lib/screens/home_screen.dart file
    )
 }
1
2
3
4
5
6

Next, look for other Widgets that repeat on multiple screens. Anything that repeats can be put into its own .dart file in a folder called /lib/shared. Then those shared widgets can be imported and reused inside any other file.

# Layout Widgets

Once you get inside the SafeArea() widget you need to start thinking about how to layout the content for your page. There are two groups of widgets for building layouts - widgets that have a child property and widgets that have a children or items property. The former is allowed to have a single child widget. The latter can hold a List of widgets.

There are many more widgets than the ones we will discuss here but this will give you enough that you can create most layouts. You will learn more through research on the flutter.dev site and experience.

# Single-Child Widgets

The Single-Child widgets are the ones that use the child property, as opposed to children. They can have only one other widget nested inside them.

To wrap an existing widget in another widget in Android Studio click on the widget, hold down Option + Enter to see the option to wrap it. In VS Code you can do the same by right-clicking on a Widget and selecting Refactor (or hitting CTRL + SHIFT + R) and select Wrap with widget....

The SafeArea is our first single-child widget. It represents all the content area in our app on each screen.

# Container

Another single-child widget is the Container widget. You can think of a Container like a <div> or <span>.

All the single-child widgets want to collapse, by default, and wrap around their child. If you just put a Text widget inside the Container, if the Container has a background colour you will see that the background colour only appears behind the Text.


Widget build(BuildContext context){
  return Scaffold(
    backgroundColor: Colors.amber,
    body: SafeArea(
      child:  Container(
        child: Text('Some text on top of the Amber background.'),
      ) //end Container
    ) //end SafeArea
  ) //end Scaffold
}
1
2
3
4
5
6
7
8
9
10
11

Notice the properties being used inside the Widgets. Scaffold has a body. SafeArea and Container have a child. However, Column has a children property. This is the difference between Single and Multi Child Widgets.

# Cards

There is a Card widget in Flutter too. You can think of it as just a basic Container but with a shadow and rounded corners added.

Card reference

# Multiple-Child Widgets

The multiple-child widgets are going to have a children property and hold a List of widgets. The List is a data structure in Dart that is very similar to a JavaScript Array or Set. A List is represented by a set of square brackets. Column, Row, ListView, BottomNavigationBar and Stack are common multi-child widgets.

The multiple-child widgets will, by default, spread to fill as much space as possible.

# Columns and Rows

The Column and Row widgets are very similar to a CSS Flexbox row or column. They both have a children property which will hold a List of <Widget>s.

Like Flexbox, they will have a main-axis and a cross-axis. A Column's main axis is in the vertical direction and the cross axis is in the horizontal direction. Flip this for a Row. There is an alignment property for each axis.

Possible values for the mainAxisAlignment property are the MainAxisAlignment enumeration values:

  • MainAxisAlignment.start
  • MainAxisAlignment.end
  • MainAxisAlignment.center
  • MainAxisAlignment.spaceAround
  • MainAxisAlignment.spaceBetween
  • MainAxisAlignment.spaceEvenly

Possible value for the crossAxisAlignment property are the CrossAxisAlignment enumeration values:

  • CrossAxisAlignment.start
  • CrossAxisAlignment.end
  • CrossAxisAlignment.center
  • CrossAxisAlignment.baseline
  • CrossAxisAlignment.stretch
//Column Widget
Column(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
      child: Text('container 1');
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.white,
      child: Text('container 2');
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
      child: Text('container 3');
    ),
  ]
)

//Row Widget
Row(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  mainAxisAlignment: MainAxisAlignment.start,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
      child: Text('container 1');
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.white,
      child: Text('container 2');
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
      child: Text('container 3');
    ),
  ]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

Sizing of the Column and Row is controlled with the mainAxisSize property. You could use mainAxisSize: MainAxisSize.min or mainAxisSize: MainAxisSize.max to tell the Column or Row collapse to the size of its contents or fill the screen width or height.

It is important to note that neither Column nor Row will scroll. If you build a column or row whose children exceed the size of the screen... then the user will not be able to get to the content. If you need to scroll, then use the ListView widget.

Sizing of the widgets inside a Column or Row can usually be controlled with a width and height property.

You can use a SizedBox(width: 20) widget to create gaps between the children in a Row or Column. Only give a width or a height, not both. Or alternatively, a Spacer widget - Spacer reference.

See also these other important widgets:

Here is an example of using an Expanded widget in a Row. The Row has 3 children. The first one has a fixed width. It will be added first to the screen. Then the remaining space will be divided between the other two Expanded widgets. The first will be given 2/3 of the remaining width. The second will get 1/3 the remaining width.

child: Row(
  children: <Widget>[
    Container(
      width: 100,
      child: Text()
    ),
    Expanded(
      flex: 2,
      child: Container( child: Text() ),
    ),
    Expanded(
      flex: 1,
      child: Container( child: Text() ),
    ),
  ]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Flutter Expanded Widget

Another important concept that you will need to understand with Flutter layouts is the concept of Unbounded Height and Width. Due to how Flutter calculates widget sizes while moving down through the Widget tree, it is important to understand this concept.

Flutter Unbounded Height and Width

Columns and rows have IntrinsicWidth and IntrinsicHeight to match heights or widths to the biggest child. So, if you have a series of children inside your Column or Row then the largest of the children will determine the cross-axis size of the Column or Row.

# ListView

As mentioned above, a ListView is similar to a Column or Row except that the user can scroll the content children automatically if the ListView exceeds the width or height of the screen. Reference for ListView.

The ListView will use a scrollDirection property set to either Axis.horizontal or Axis.vertical to determine which way items can be scrolled. Axis.vertical is the default.

ListView(
  children: <Widget>[
    Container(
      height: 50,
      color: Colors.amber[600],
      child: const Center(child: Text('Entry A')),
    ),
    Container(
      height: 50,
      color: Colors.amber[500],
      child: const Center(child: Text('Entry B')),
    ),
    Container(
      height: 50,
      color: Colors.amber[100],
      child: const Center(child: Text('Entry C')),
    ),
  ],
  addAutomaticKeepAlives: false,
  cacheExtent: 50,
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

The above example builds a static ListView that has 3 children. All the data is hard-coded into the widgets.

The addAutomaticKeepAlives and cacheExtent properties are about improving performance by limiting the amount of items that are kept in memory or garbage collected.

Another way to build a ListView is by having one or more List variables that hold the information to be used in the ListView. There is a different constructor to use to make the ListView - ListView.builder.

The following example builds the exact same ListView but uses the two List variables from the top to dynamically create all the children for the ListView.

final List<String> entries = <String>['A', 'B', 'C'];
final List<int> colorCodes = <int>[600, 500, 100];

ListView.builder(
  itemCount: entries.length, //needed if using itemBuilder
  itemBuilder: (BuildContext context, int index) {
    return Container(
      height: 50,
      color: Colors.amber[colorCodes[index]],
      child: Center(child: Text('Entry ${entries[index]}')),
    );
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13

Flutter ListView widget

Another alternative to the ListView.builder is to use an AnimatedList which will add some smooth transitions when you dynamically add or remove items from your data list. Reference for AnimatedList. It also uses an itemBuilder property.

Flutter AnimatedList widget

The example above used Container for each of the children in the ListView. Another common widget to use inside a ListView is a ListTile. Reference for ListTile

A ListTile widget is similar to an AppBar widget. It is a horizontal Row of content broken into three areas. The left most is a place where you could add an Icon, IconButton, or CircleAvatar. The middle section is for your text. It has a primary and secondary space for text. Then the right side is used for action buttons.

ListTile(
  leading: FlutterLogo(),
  title: Text('Some bigger text'),
  subtitle: Text('some smaller fainter text'),
  trailing: Icon(Icons.more_vert),
  dense: false,
  enabled: true,
  onLongPress: () => _pressCallback,
),
1
2
3
4
5
6
7
8
9

Flutter ListTile Widget

# SingleChildScrollView

When you have a static vertical list of content, think Column(), and you are adding the content to that series of vertical elements, you need to worry about the sizes.

You could design a screen that has a few Images and Text. It might fit and work well on 90% of the device screens out there. But there will always be a few that cause problems.

For the situations where your Column is a few pixels too large, we have the SingleChildScrollView() widget. As the name implies, this widget only takes a single child. And additionally, as the name implies, it allows us to scroll the screen and avoid the problems created by the slightly too tall children.

SingleChildScrollView reference

# Stack

The Stack() widget is for creating a layout with overlapping elements. Think of a FAB that floats on top of other content or the NavigationStack that we used with React Native. If you want to build a non-standard layout with elements positioned not in columns or rows or grids but placed based on other considerations, then the Stack is what you want. It is combined with the Positioned() widget to create the layout.

We will not be using these widgets explicitly in our projects but they are useful to know about.

Stack widget reference

Positioned widget reference

Both those reference pages have videos that demonstrate their usage.

# Padding and Margin

Padding and margins in all our layout widgets are controlled with EdgeInsets.

Regardless of whether you are setting the value for a margin property or a padding property, you will use one of these EdgeInset methods:

Container(
  child: Text('hello world'),
  margin: EdgeInsets.symmetric(vertical: 50.0, horizontal: 10.0),
  //two values - one for vertical and one for horizontal
),
Container(
  child: Text('hello world'),
  padding: EdgeInsets.all(20.0),
  //same value on all 4 sides
),
Container(
  child: Text('hello world'),
  margin: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
  //4 values left, top, right, bottom
),
Container(
  child: Text('hello world'),
  padding: EdgeInsets.fromSTEB(10.0, 20.0, 30.0, 40.0),
  //4 values start top end bottom
),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

EdgeInsets reference

# Padding Widget

Because not every widget will have a padding or margin property, Flutter has a Padding widget that we can use to wrap practically any other UI widget.

Flutter Padding Widget

Padding widget reference

# Align Widget

When you need to align a widget within some containing parent widget, there will be times that you don't have direct access to properties like align. To solve this issue, there is an Align widget that you can use to align your widget inside its parent widget.

It has an alignment property that can be used with the Alignment class, which has a series of position names like Alignment.topCenter.

Align(
  alignment: Alignment.bottomRight,
  child: Text('Put me somewhere')
)
1
2
3
4

Flutter Align Widget

Here is the Align widget reference and the Alignment class reference. Look at the Constants section on the Alignment class page for a list of the position names.

# What to do this week

TODO Things to do before next week

  • Read and watch all the content for modules 10.1 and 10.2
  • Start working on Flutter Assignment 1
  • Practice working with Flutter Widgets
Last Updated: 11/28/2023, 9:23:36 PM