10.2 Flutter Layout Widgets
# 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 (opens new window) 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 (opens new window) 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 (opens new window)
Here is a YouTube Playlist on Widgets (opens new window) 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(...),
)
)
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'),
),
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),
),
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,
),
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
)
}
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 selectingRefactor
(or hittingCTRL + SHIFT + R
) and selectWrap 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
}
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 (opens new window)
# 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');
),
]
)
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 (opens new window).
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() ),
),
]
)
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 (opens new window).
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,
)
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]}')),
);
}
);
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 (opens new window). 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 (opens new window)
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,
),
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 (opens new window)
# 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 (opens new window)
Positioned widget reference (opens new window)
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
),
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
EdgeInsets reference (opens new window)
# 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
# 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')
)
2
3
4
Flutter Align Widget
Here is the Align widget reference (opens new window) and the Alignment class reference (opens new window). 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