8 Creating an App's Navigation
WHAT YOU WILL LEARN IN THIS CHAPTER
How to use the Navigator
widget to navigate between pages
How hero animation allows a widget transition to fly into place from one page to another
How to display a horizontal list of BottomNavigationBarItem
s containing an icon
and a title
at the bottom of the page
How to enhance the look of a bottom navigation bar with the BottomAppBar
widget, which allows enabling a notch
How to display a horizontal row of tabs with TabBar
How to use TabBarView
in conjunction with TabBar
to display the page of the selected tab
How Drawer
allows the user to slide a panel from left or right
How to use the ListView
constructor to build a short list of items quickly
How to use the ListView
constructor with the Drawer
widget to show a menu list
In this chapter, you'll learn that navigation is a major component in a mobile application. Good navigation creates a great user experience (UX) by making it easy to access information. For example, imagine making a journal entry, and when trying to select a tag, it's not available, so you need to create a new one. Do you close the entry and go to Settings ➪ Tags to add a new one? That would be clunky. Instead, the user needs the ability to add a new tag on the fly and appropriately navigate to select or add a tag from their current position. When designing an app, always keep in mind how the user would navigate to different parts of the app with the least number of taps.
Animation while navigating to different pages is also important if it helps to convey an action, rather than simply being a distraction. What does this mean? Just because you can show fancy animations does not mean you should. Use animations to enhance the UX, not frustrate the user.
USING THE NAVIGATOR
The Navigator
widget manages a stack of routes to move between pages. You can optionally pass data to the destination page and back to the original page. To start navigating between pages, you use the Navigator.push
, pushNamed
, and pop
methods. (You'll learn how to use the pushNamed
method in the “Using the Named Navigator Route” section of this chapter.) Navigator
is incredibly smart; it shows native navigation on iOS or Android. For example, in iOS when navigating to a new page, you usually slide the next page from the right side of the screen toward the left. In Android when navigating to a new page, you typically slide the next page from the bottom of the screen toward the top. To summarize, in iOS, the new page slides in from the right, and in Android, it slides in from the bottom.
The following example shows you how to use the Navigator.push
method to navigate to the About page. The push
method passes the BuildContext
and Route
arguments. To push a new Route
argument, you create an instance of the MaterialPageRoute
class that replaces the screen with the appropriate platform (iOS or Android) animation transition. In the example, the fullscreenDialog
property is set to true
to present the About page as a full‐screen modal dialog. By setting the fullscreenDialog
property to true
, the About page app bar automatically includes a close button. In iOS, the modal dialog transition presents the page by sliding from the bottom of the screen toward the top, and this is also the default for Android.
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) => About(),
),
);
The following example shows how to use the Navigator.pop
method to close the page and navigate back to the previous page. You call the Navigator.pop(context)
method by passing the BuildContext
argument, and the page closes by sliding from the top of the screen toward the bottom. The second example shows how to pass a value back to the previous page.
// Close page
Navigator.pop(context);
// Close page and pass a value back to previous page
Navigator.pop(context, 'Done');
TRY IT OUT Creating the Navigator App, Part 1—The About Page
The project has a main home page with a FloatingActionButton
to navigate to a gratitude page by passing a default selected Radio
button value. The gratitude page shows three Radio
buttons to select a value and then pass it back to the home page and update the Text
widget with the appropriate value. The AppBar
has an actions
IconButton
that navigates to the about page by passing a fullscreenDialog
argument set to true
to create a full‐screen modal dialog. The modal dialog shows a close button at the upper left of the page and animates from the bottom. In this first part, you'll develop the navigation from the main page to the About page and back.
Create a new Flutter project and name it ch8_navigator
. Refer to the instructions in Chapter 4 , “Creating a Starter Project Template.” For this project, you need to create only the pages
folder.
Open the home.dart
file and add an IconButton
to the AppBar
actions
Widget
list.
The IconButton
onPressed
property will call the method _openPageAbout()
and pass context
and fullscreenDialog
arguments. Do not worry about the squiggly red lines under the method name; you'll create that method in later steps. The context
argument is needed by the Navigator
widget, and the fullscreenDialog
argument is set to true
to show the About page as a full‐screen modal. If you set the fullscreenDialog
argument to false
, the About page shows a back arrow instead of a close button icon.
appBar: AppBar(
title: Text('Navigator'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.info_outline),
onPressed: () => _openPageAbout(
context: context,
fullscreenDialog: true,
),
),
],
),
Add a SafeArea
with Padding
as a child
to the body.body: SafeArea(
child: Padding(),
),
In the Padding
child
, add a Text()
widget, with the text 'Grateful
for:
$_howAreYou'
. Notice the $_howAreYou
variable, which will contain the returned value when navigating back (Navigator.pop
) from the gratitude page. Add a TextStyle
class with a fontSize
value of 32.0 pixels.body: SafeArea(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text('Grateful for: $_howAreYou', style: TextStyle(fontSize: 32.0),),
),
),
To the Scaffold
floatingActionButton
property, add a FloatingActionButton()
widget with an onPressed
that calls the method _openPageGratitude(context:
context)
, which passes the context
argument.
For the FloatingActionButton()
child
, add the icon called sentiment_satisfied
, and for the tooltip
, add the 'About'
description.floatingActionButton: FloatingActionButton(
onPressed: () => _openPageGratitude(context: context),
tooltip: 'About',
child: Icon(Icons.sentiment_satisfied),
),
On the first line after the _HomeState
class definition, add a String
variable called _howAreYou
with a default value of '…'
.String _howAreYou = "…";
Continue by adding the _openPageAbout()
method that accepts BuildContext
and bool
named parameters with the default values set to false
.void _openPageAbout({BuildContext context, bool fullscreenDialog = false}) {}
In the openPageAbout()
method, add a Navigator.push()
method with a context
and a second argument of MaterialPageRoute()
.The MaterialPageRoute()
class passes the fullscreenDialog
argument and a builder
that calls the About()
page that you'll create in later steps.void _openPageAbout({BuildContext context, bool fullscreenDialog = false}) {
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: fullscreenDialog,
builder: (context) => About(),
),
);
}
On top of the home.dart
page, import the about.dart
page that you'll create next.import 'about.dart';
Navigator
is simple to use but also powerful. Let's examine how it works. Navigator.push()
passes two arguments: context
and MaterialPageRoute
. For the first argument, you pass the context
argument. The second argument, MaterialPageRoute()
, gives you the horsepower to navigate to another page using platform‐specific animation. Only the builder is required to navigate with the optional fullscreenDialog
argument.
Create a new file called about.dart
in the lib/pages
folder. Since this page only displays information, create a StatelessWidget
class called About
.
For the body
, add the usual SafeArea
with Padding
and the child
property as a Text
widget.// about.dart
import 'package:flutter/material.dart';
class About extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('About'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('About Page'),
),
),
);
}
}
How It Works
You add to the AppBar
an IconButton
under the actions
property. The icon property for the IconButton
is set to Icons.info_outline
, the _openPageAbout()
method passes the context
, and the fullscreenDialog
argument is set to true
. You also add to the Scaffold
a FloatingActionButton
that calls the _openPageGratitude()
method. The _openPageAbout()
method uses the Navigator.push()
method to pass the context
and the MaterialPageRoute
. The MaterialPageRoute
passes the fullscreenDialog
argument set to true
, and the builder
calls the About()
page. The About
page class is a StatelessWidget
with a Scaffold
and AppBar
; the body
property has a SafeArea
with Padding
as a child
that shows a Text
widget with the “About Page” text.
TRY IT OUT Creating the Navigator App, Part 2—The Gratitude Page
The second part of the app is to navigate to the gratitude page by passing a default value to select the appropriate Radio
button. Once you navigate back to the home page, the newly selected Radio
button value is passed back and displayed in the Text
widget.
Open the home.dart
file, and after the _openPageAbout()
method, add the _openPageGratitude()
method. The _openPageGratitude()
method takes two parameters: a context
and a fullscreenDialog
bool
variable with a default value of false
. In this case, the gratitude page is not a fullscreenDialog
. Like the previous MaterialPageRoute
, the builder
opens the page. In this case, it is the gratitude page.Note that when passing data to the gratitude page and waiting to receive a response, the method is marked as async
to wait a response from Navigator.push
by using the await
keyword.
void _openPageGratitude(
{BuildContext context, bool fullscreenDialog = false}) async {
final String _gratitudeResponse = await Navigator.push(
The MaterialPageRoute
builder
builds the contents of the route. In this case, the content is the gratitude page, which accepts a radioGroupValue
int
parameter with a value of −1. The −1 value tells the Gratitude
class page not to select any Radio
buttons. If you pass a value like 2, it selects the appropriate Radio
button that corresponds to this value
.
builder: (context) => Gratitude(
radioGroupValue: -1,
),
Once the user dismisses the gratitude page, the _gratitudeResponse
variable is populated. Use the ??
(double question marks if null
) operator to check the _gratitudeResponse
for a valid value (not null
) and populate the _howAreYou
variable. The Text
widget is populated with the _howAreYou
value on the home page with the appropriate selected gratitude value or an empty string. In other words, if the _gratitudeResponse
value is not null
the _howAreYou
variable is populated with the_gratitudeResponse
value; otherwise the _howAreYou
variable is populated by an empty string.
_howAreYou = _gratitudeResponse ?? '';
Here's the full _openPageGratitude()
method code:
void _openPageGratitude(
{BuildContext context, bool fullscreenDialog = false}) async {
final String _gratitudeResponse = await Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: fullscreenDialog,
builder: (context) => Gratitude(
radioGroupValue: -1,
),
),
);
_howAreYou = _gratitudeResponse ?? '';
}
At the top of the home.dart
page, add the gratitude.dart
page that you'll create next.import 'gratitude.dart';
Create a new file named gratitude.dart
in the lib/pages
folder. Since this page will modify data (state), create a StatefulWidget
class called Gratitude
.To receive data passed from the home page, modify the Gratitude
class by adding a final
int
variable named radioGroupValue
. Note the final variable does not start with an underscore. Create a named constructor requiring this parameter. The radioGroupValue
variable is accessed by the class _GratitudeState
extends
State<Gratitude>
by calling widget.radioGroupValue
.
class Gratitude extends StatefulWidget {
final int radioGroupValue;
Gratitude({Key key, @required this.radioGroupValue}) : super(key: key);
@override
_GratitudeState createState() => _GratitudeState();
}
For the Scaffold
AppBar
, add an IconButton
to the actions
list of Widget
. Set the IconButton
icon
to Icons.check
with the onPressed
property calling the Navigator.pop
, which returns the _selectedGratitude
to the home page.appBar: AppBar(
title: Text('Gratitude'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: () => Navigator.pop(context, _selectedGratitude),
),
],
),
For the body
, add the usual SafeArea
and Padding
with child
property as a Row
.The Row
children
list of Widget
contains three alternating Radio
and Text
widgets. The Radio
widget takes the value
, groupValue
, and onChanged
properties. The value
property is the ID value for the Radio
button. The groupValue
property holds the value of the currently selected Radio
button. The onChanged
passes the selected index
value to the custom method _radioOnChanged()
that handles which Radio
button is currently selected. Following each Radio
button, there is a Text
widget that acts as a label for the Radio
button.
Here's the full body
source code:
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: <Widget>[
Radio(
value: 0,
groupValue: _radioGroupValue,
onChanged: (index) => _radioOnChanged(index),
),
Text('Family'),
Radio(
value: 1,
groupValue: _radioGroupValue,
onChanged: (index) => _radioOnChanged(index),
),
Text('Friends'),
Radio(
value: 2,
groupValue: _radioGroupValue,
onChanged: (index) => _radioOnChanged(index),
),
Text('Coffee'),
],
),
),
),
On the first line after the _HomeState
class definition, add three variables—_gratitudeList
, _selectedGratitude
, and _radioGroupValue
—and the _radioOnChanged()
method.
Create the _radioOnChanged()
method taking an int
for the selected index
of the Radio
button. In the method, call setState()
to have the Radio
widgets update with the selected value
. The _radioGroupValue
variable is updated with the index
. The _selectedGratitude
variable (example value Coffee
) is updated by taking the _gratitudeList[index]
list value by the selected index
(position in the list).void _radioOnChanged(int index) {
setState(() {
_radioGroupValue = index;
_selectedGratitude = _gratitudeList[index];
print('_selectedRadioValue $_selectedGratitude');
});
}
Override initState()
to initialize the _gratitudeList
. Since the _radioGroupValue
is passed from the home page, initialize it with widget.radioGroupValue
, which is the final variable passed from the home page._gratitudeList..add('Family')..add('Friends')..add('Coffee');
_radioGroupValue = widget.radioGroupValue;
The following is the code that declares all variables and methods:
class _GratitudeState extends State<Gratitude> {
List<String> _gratitudeList = List();
String _selectedGratitude;
int _radioGroupValue;
void _radioOnChanged(int index) {
setState(() {
_radioGroupValue = index;
_selectedGratitude = _gratitudeList[index];
print('_selectedRadioValue $_selectedGratitude');
});
}
@override
void initState() {
super.initState();
_gratitudeList..add('Family')..add('Friends')..add('Coffee');
_radioGroupValue = widget.radioGroupValue;
}
Here's the entire gratitude.dart
file source code:
import 'package:flutter/material.dart';
class Gratitude extends StatefulWidget {
final int radioGroupValue;
Gratitude({Key key, @required this.radioGroupValue}) : super(key: key);
@override
_GratitudeState createState() => _GratitudeState();
}
class _GratitudeState extends State<Gratitude> {
List<String> _gratitudeList = List();
String _selectedGratitude;
int _radioGroupValue;
void _radioOnChanged(int index) {
setState(() {
_radioGroupValue = index;
_selectedGratitude = _gratitudeList[index];
print('_selectedRadioValue $_selectedGratitude');
});
}
@override
void initState() {
super.initState();
_gratitudeList..add('Family')..add('Friends')..add('Coffee');
_radioGroupValue = widget.radioGroupValue;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Gratitude'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: () => Navigator.pop(context, _selectedGratitude),
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: <Widget>[
Radio(
value: 0,
groupValue: _radioGroupValue,
onChanged: (index) => _radioOnChanged(index),
),
Text('Family'),
Radio(
value: 1,
groupValue: _radioGroupValue,
onChanged: (index) => _radioOnChanged(index),
),
Text('Friends'),
Radio(
value: 2,
groupValue: _radioGroupValue,
onChanged: (index) => _radioOnChanged(index),
),
Text('Coffee'),
],
),
),
),
);
}
}
How It Works
You have the entire app created with a home page that can navigate to the about page as a fullscreenDialog
. The fullscreenDialog
gives the about page a default close action button. By tapping the home page's FloatingActionButton
, the Navigator
MaterialPageRoute
builder
builds the contents for the route, in this case the gratitude page. Through the Gratitude
constructor
, data is passed to unselected Radio
buttons. From the gratitude page, a list of Radio
buttons gives a choice to select a gratitude. By tapping the AppBar
action button (checkbox IconButton
), the Navigator.pop
method passes the selected gratitude value back to the Home
Text
widget. From the home page, you called the Navigator.push
method by using the await
keyword, and the method has been waiting to receive a value. Once the About page's Navigator.pop
method is called, it returns a value to the home page's _gratitudeResponse
variable. Using the await
keyword is a powerful and straightforward feature to implement.
Using the Named Navigator Route
An alternate way to use Navigator
is to refer to the page that you are navigating to by the route name. The route name starts with a slash, and then comes the route name. For example, the About page route name is '/about'
. The list of routes
is built into the MaterialApp()
widget. The routes
have a Map
of String
and WidgetBuilder
where the String
is the route name, and the WidgetBuilder
has a builder
to build the contents of the route by the Class
name (About) of the page to open.
routes: <String, WidgetBuilder>{
'/about': (BuildContext context) => About(),
'/gratitude': (BuildContext context) => Gratitude(),
},
To call the route, the Navigator.pushNamed()
method is called by passing two arguments. The first argument is context
, and the second is the route
name.
Navigator.pushNamed(context, '/about');
USING HERO ANIMATION
The Hero
widget is a great out‐of‐the‐box animation to convey the navigation action of a widget flying into place from one page to another. The hero animation is a shared element transition (animation) between two different pages.
To visualize the animation, imagine seeing a superhero flying into action. For example, you have a list of journal entries with a photo thumbnail, the user selects an entry, and you see the photo thumbnail transition to the detail page by moving and growing to full size. The photo thumbnail is the superhero, and when tapped, it flies into action by moving from the list page to the detail page and lands perfectly on the correct location at the top of the detail page showing the full photo. When the detail page is dismissed, the Hero
widget flies back to the original page, position, and size. In other words, the animation shows the photo thumbnail moving and growing into place from the list page to the detail page, and once the detail page is dismissed, the animation and size are reversed. The Hero
widget has all of these features built in; there's no need to write custom code to handle the size and animation between pages.
To continue with the previous scenario, you wrap the list page Image
widget as a child
of the Hero
widget and assign a tag
property name. Repeat the same steps for the detail page and make sure the tag
property value is the same in both pages.
// List page
Hero(
tag: 'photo1',
child: Image(
image: AssetImage("assets/images/coffee.png"),
),
),
// Detail page
Hero(
tag: 'photo1',
child: Container(
child: Image(
image: AssetImage("assets/images/coffee.png"),
),
),
),
The Hero
child
widget is marked for hero animation. When the Navigator
pushes or pops a PageRoute
, the entire screen's content is replaced. This means during the animation transition the Hero
widget is not shown in the original position in both the old and new routes, but it moves and resizes from one page to another. Each Hero
tag must be unique and match on both the originating and landing pages.
TRY IT OUT Creating the Hero Animation App
In this example, the Hero
widget has an Icon
as a child
wrapped in a GestureDetector
. An InkWell
could also be used instead of the GestureDetector
to show a material tap animation. The InkWell
widget is a Material Component that responds to touch gestures by displaying a splash (ripple) effect. You'll take a more in‐depth look at GestureDetector
and InkWell
in Chapter 11 , “Applying Interactivity.” When the Icon
is tapped, Navigator.push
is called to navigate to the detail screen, called Fly
. Since the Hero
widget animation is like a superhero flying, the detail screen is named Fly
.
Create a new Flutter project and name it ch8_hero_animation
. Again, you can follow the instructions in Chapter 4 . For this project, you need to create only the pages
folder.
Open the home.dart
file and add to the body
a SafeArea
with Padding
as a child
.body: SafeArea(
child: Padding(),
),
In the Padding
child
, add the GestureDetector()
widget, which you will create next.body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: GestureDetector(),
),
),
Add to the GestureDetector
child
a Hero
widget with tag
'format_paint'
; tag
can be any unique ID. The Hero
widget child
is a format_paint
icon with lightGreen
color
and a size
value of 120.0 pixels. Note you could have used an InkWell()
widget instead of GestureDetector()
. The InkWell()
widget shows a splash feedback where tapped, but the GestureDetector()
widget will not show the touch feedback. For the onTap
property, you call Navigator.push
to open the detail page called Fly
.GestureDetector(
child: Hero(
tag: 'format_paint',
child: Icon(
Icons.format_paint,
color: Colors.lightGreen,
size: 120.0,
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Fly()),
);
},
),
At the top of the home.dart
page, import the fly.dart
page that you will create next.
import 'fly.dart';
Here's the entire Home.dart
file source code:
import 'package:flutter/material.dart';
import 'fly.dart';
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hero Animation'),
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(16.0),
child: GestureDetector(
child: Hero(
tag: 'format_paint',
child: Icon(
Icons.format_paint,
color: Colors.lightGreen,
size: 120.0,
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Fly()),
);
},
),
),
),
);
}
}
Create a new file called fly.dart
in the lib/pages
folder. Since this page only displays information, create a StatelessWidget
class called Fly
. For the body
, add the usual SafeArea
with a child
set to the Hero()
widget.body: SafeArea(
child: Hero(),
),
To calculate the width
of the Icon
, after Widget
build(BuildContext
context)
{
, add a double
variable called _width
set by MediaQuery.of(context).size.shortestSide
/
2
. The shortestSide
property returns the lesser of the width or height of the screen, and you divide by two to make it half the size.The reason you calculate the width is only to resize the Icon
width
according to device size and orientation. If you used an Image
instead, this calculation would not be necessary; it can be done by using BoxFit.fitWidth
.
double _width = MediaQuery.of(context).size.shortestSide / 2;
Add to the Hero
widget a tag
of 'format_paint'
with a Container
for the child
. Note for the Hero
widget to work properly, the tag
needs to be the same name you gave in the GestureDetector
child
Hero
widget in the home.dart
file. The Container
child
is a format_paint
icon with lightGreen
color
and a size
value of the width
variable.
For the Container
alignment
property, use Alignment.bottomCenter
. You can experiment using different Alignment
values to see the hero animation variations at work.
Hero(
tag: 'format_paint',
child: Container(
alignment: Alignment.bottomCenter,
child: Icon(
Icons.format_paint,
color: Colors.lightGreen,
size: _width,
),
),
)
Here's the entire Fly.dart
file source code:
import 'package:flutter/material.dart';
class Fly extends StatelessWidget {
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.shortestSide / 2;
return Scaffold(
appBar: AppBar(
title: Text('Fly'),
),
body: SafeArea(
child: Hero(
tag: 'format_paint',
child: Container(
alignment: Alignment.bottomCenter,
child: Icon(
Icons.format_paint,
color: Colors.lightGreen,
size: _width,
),
),
),
),
);
}
}
How It Works
The hero animation is a powerful built‐in animation to convey an action by automatically animating a widget from one page to another to the correct size and position. On the home page, you declare a GestureDetector
with the Hero
widget as the child
widget. The Hero
widget sets an Icon
as the child
. The onTap
calls the Navigator.push()
method, which navigates to the page Fly
. All you need to do on the Fly
page is to declare the widget you're animating to as a child
of the Hero
widget. When you navigate back to the home page, the Hero
animates the Icon
to the original position.
USING THE BOTTOMNAVIGATIONBAR
BottomNavigationBar
is a Material Design widget that displays a list of BottomNavigationBarItem
s that contains an icon
and a title
at the bottom of the page (Figure 8.1 ). When the BottomNavigationBarItem
is selected, the appropriate page is built.
FIGURE 8.1 : Final BottomNavigationBar with icons and titles
TRY IT OUT Creating the BottomNavigationBar App
In this example, BottomNavigationBar
has three BottomNavigationBarItem
s that replace the current page with the selected one. There are different ways to display the selected page; you use a Widget
class as a variable.
Create a new Flutter project and name it ch8_bottom_navigation_bar
. As always, you can follow the instructions in Chapter 4 . For this project, you need to create only the pages
folder.
Open the home.dart
file, and add to the body
a SafeArea
with Padding
as a child
.body: SafeArea(
child: Padding(),
),
In the Padding
child
, add the variable Widget
_currentPage
, which you will create next. Note this time you are using a Widget
class to create the _currentPage
variable that holds each page that is selected, either the Gratitude
, Reminders
, or Birthdays
StatelessWidget
class.body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _ currentPage,
),
),
Add to the Scaffold
bottomNavigationBar
property a BottomNavigationBar
widget. For the currentIndex
property, use the _currentIndex
variable, which will be created later.bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
The items
property is a List
of BottomNavigationBarItem
s. Each BottomNavigationBarItem
takes an icon
property and a title
property.
Add to the items
property three BottomNavigationBarItem
s with icon
cake
, sentiment
_satisfied
, and access_alarm
. The title
s
are
'Birthdays'
, 'Gratitude'
, and 'Reminders'
.items: [
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('Birthdays'),
),
For the onTap
property, the callback
returns the current index of the active item. Name the variable selectedIndex
.onTap: (selectedIndex) => _changePage(selectedIndex),
Here's the entire BottomNavigationBar
code:
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('Birthdays'),
),
BottomNavigationBarItem(
icon: Icon(Icons.sentiment_satisfied),
title: Text('Gratitude'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Reminders'),
),
],
onTap: (selectedIndex) => _changePage(selectedIndex),
),
Add the _changePage(int
selectedIndex)
method after Scaffold()
. The
_changePage()
method accepts
an
int
value
of
the
selected
index.
The selectedIndex
is used with the setState()
method to set the _currentIndex
and _currentPage
variables.The _currentIndex
equals the selectedIndex
, and the _currentPage
equals the page from the List
_listPages
that corresponds to the selected index.
It's important to note that the Widget
_currentPage
variable displays each page selected without the need for a Navigator
widget. This is a great example of the power of customizing widgets to your needs.
void _changePage(int selectedIndex) {
setState(() {
_currentIndex = selectedIndex;
_currentPage = _listPages[selectedIndex];
});
}
On the first line after the _HomeState
class definition, add the variables _currentIndex
, _listPages
, and _currentPage
. The _listPages
List
holds each page's Class
name.int _currentIndex = 0;
List _listPages = List();
Widget _currentPage;
Override initState()
to add each page to the _listPages
List
and initialize the _currentPage
with the Birthdays()
page. Note the use of cascade notation; the double dots allow you to perform a sequence of operations on the same object.@override
void initState() {
super.initState();
_listPages
..add(Birthdays())
..add(Gratitude())
..add(Reminders());
_currentPage = Birthdays();
}
Add to the top of the home.dart
file the imports for each page that will be created next.import 'package:flutter/material.dart';
import 'gratitude.dart';
import 'reminders.dart';
import 'birthdays.dart';
Here's the entire home.dart
file:
import 'package:flutter/material.dart';
import 'gratitude.dart';
import 'reminders.dart';
import 'birthdays.dart';
class Home extends StatefulWidget {
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _currentIndex = 0;
List _listPages = List();
Widget _currentPage;
@override
void initState() {
super.initState();
_listPages
..add(Birthdays())
..add(Gratitude())
..add(Reminders());
_currentPage = Birthdays();
}
void _changePage(int selectedIndex) {
setState(() {
_currentIndex = selectedIndex;
_currentPage = _listPages[selectedIndex];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('BottomNavigationBar'),
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(16.0),
child: _currentPage,
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('Birthdays'),
),
BottomNavigationBarItem(
icon: Icon(Icons.sentiment_satisfied),
title: Text('Gratitude'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Reminders'),
),
],
onTap: (selectedIndex) => _changePage(selectedIndex),
),
);
}
}
Create three StatelessWidget
pages and call them Birthdays
, Gratitude
, and Reminders
. Each page will have a Scaffold
with Center()
for the body
. The Center
child
is an Icon
with a size
value of 120.0 pixels and a color
property. Here are the three Dart files, birthdays.dart
, gratitude.dart
, and reminders.dart
:// birthdays.dart
import 'package:flutter/material.dart';
class Birthdays extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.cake,
size: 120.0,
color: Colors.orange,
),
),
);
}
}
// gratitude.dart
import 'package:flutter/material.dart';
class Gratitude extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.sentiment_satisfied,
size: 120.0,
color: Colors.lightGreen,
),
),
);
}
}
// reminders.dart
import 'package:flutter/material.dart';
class Reminders extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.access_alarm,
size: 120.0,
color: Colors.purple,
),
),
);
}
}
How It Works
The BottomNavigationBar
items
property has a List
of three BottomNavigationBarItem
s. For each BottomNavigationBarItem
, you set an icon
property and a title
property. The Bottom NavigationBar
onTap
passes the selected index value to the _changePage
method. The _changePage
method uses setState()
to set the _currentIndex
and _currentPage
to display. The _currentIndex
sets the selected BottomNavigationBarItem
, and the _currentPage
sets the current page to display from the _listPages
List
.
USING THE BOTTOMAPPBAR
The BottomAppBar
widget behaves similarly to the BottomNavigationBar
, but it has an optional notch along the top. By adding a FloatingActionButton
and enabling the notch, the notch provides a nice 3D effect so it looks like the button is recessed into the navigation bar (Figure 8.2 ). For example, to enable the notch, you set the BottomAppBar
shape
property to a NotchedShape
class like the CircularNotchedRectangle()
class and set the Scaffold
floatingActionButtonLocation
property to FloatingActionButtonLocation.endDocked
or centerDocked
. Add to the Scaffold
floatingActionButton
property a FloatingActionButton
widget, and the result shows the FloatingActionButton
embedded into the BottomAppBar
widget, which is the notch.
FIGURE 8.2 : BottomAppBar with embedded FloatingActionButton creating a notch
BottomAppBar(
shape: CircularNotchedRectangle(),
)
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
),
TRY IT OUT Creating the BottomAppBar App
In this example, the BottomAppBar
has a Row
as a child with three IconButton
s to show selection items. The main objective is to use a FloatingActionButton
to dock it to the BottomAppBar
with a notch. The notch is enabled by the BottomAppBar
shape
property set to CircularNotchedRectangle()
.
Create a new Flutter project and name it ch8_bottom_app_bar
. Again, you can follow the instructions in Chapter 4 . For this project, you only need to create the pages
folder.
Open the home.dart
file, and add to the body
a SafeArea
with a Container
as a child
.body: SafeArea(
child: Container(),
),
Add a BottomAppBar()
widget to the Scaffold
bottomNavigationBar
property.bottomNavigationBar: BottomAppBar(),
To enable the notch, you set two properties.
First set the BottomAppBar
shape
property to CircularNotchedRectangle()
. Set the color
property to Colors.blue.shade200
and add a Row
as a child
.
Next set floatingActionButtonLocation
, which you will handle in step 7.
bottomNavigationBar: BottomAppBar(
color: Colors.blue.shade200,
shape: CircularNotchedRectangle(),
child: Row(),
),
Continue by adding to the Row
a mainAxisAlignment
property as MainAxisAlignment.spaceAround
. The spaceAround
constant allows the IconButton
s to have even spacing between them.bottomNavigationBar: BottomAppBar(
color: Colors.blue.shade200,
shape: CircularNotchedRectangle(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
],
),
),
Add three IconButton
s to the Row
children
list. After the last IconButton
, add a Divider()
to add an even space to the right since FloatingActionButton
is docked on the right side of the BottomAppBar
. Instead of a Divider()
, you could have used a Container
with a width
property.bottomNavigationBar: BottomAppBar(
color: Colors.blue.shade200,
shape: CircularNotchedRectangle(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
IconButton(
icon: Icon(Icons.access_alarm),
color: Colors.white,
onPressed: (){},
),
IconButton(
icon: Icon(Icons.bookmark_border),
color: Colors.white,
onPressed: (){},
),
IconButton(
icon: Icon(Icons.flight),
color: Colors.white,
onPressed: (){},
),
Divider(),
],
),
),
Set the notch location of the floatingActionButtonLocation
property to FloatingActionButtonLocation.endDocked
. You could also set it to centerDocked
.floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
Add a FloatingActionButton
to the floatingActionButton
property.floatingActionButton: FloatingActionButton(
backgroundColor: Colors.blue.shade200,
onPressed: () {},
child: Icon(Icons.add),
),
Here's the entire home.dart
file source code:
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('BottomAppBar'),
),
body: SafeArea(
child: Container(),
),
bottomNavigationBar: BottomAppBar(
color: Colors.blue.shade200,
shape: CircularNotchedRectangle(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
IconButton(
icon: Icon(Icons.access_alarm),
color: Colors.white,
onPressed: (){},
),
IconButton(
icon: Icon(Icons.bookmark_border),
color: Colors.white,
onPressed: (){},
),
IconButton(
icon: Icon(Icons.flight),
color: Colors.white,
onPressed: (){},
),
Divider(),
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.blue.shade200,
onPressed: () {},
child: Icon(Icons.add),
),
);
}
}
How It Works
To enable the notch, two properties need to be set for the Scaffold
widget. The first is to use a BottomAppBar
with the shape
property set to CircularNotchedRectangle()
. The second is to set the floatingActionButtonLocation
property to FloatingActionButtonLocation.endDocked
or centerDocked
.
USING THE TABBAR AND TABBARVIEW
The TabBar
widget is a Material Design widget that displays a horizontal row of tabs. The tabs
property takes a List
of Widgets
, and you add tabs by using the Tab
widget. Instead of using the Tab
widget, you could create a custom widget, which shows the power of Flutter. The selected Tab
is marked with a bottom selection line.
The TabBarView
widget is used in conjunction with the TabBar
widget to display the page of the selected tab. Users can swipe left or right to change content or tap each Tab
.
Both the TabBar
(Figure 8.3 ) and TabBarView
widgets take a controller
property of TabController
. The TabController
is responsible for syncing tab selections between a TabBar
and a TabBarView
. Since the TabController
syncs the tab selections, you need to declare the SingleTickerProviderStateMixin
to the class. In Chapter 7 , “Adding Animation to an App,” you learned how to implement the Ticker
class that is driven by the ScheduleBinding.scheduleFrameCallback
reporting once per animation frame. It is trying to sync the animation to be as smooth as possible.
FIGURE 8.3 : TabBar in the Scaffold bottomNavigationBar property
TRY IT OUT Creating the TabBar and TabBarView App
In this example, the TabBar
widget is the child of a bottomNavigationBar
property. This places the TabBar
at the bottom of the screen, but you could also place it in the AppBar
or a custom location. When you use a TabBar
in combination with the TabBarView
, once a Tab
is selected, it automatically displays the appropriate content. In this project, the content is represented by three separate pages. You'll create the same three pages as you did in the BottomNavigationBar
project.
Create a new Flutter project and name it ch8_tabbar
. Once more, you can follow the instructions in Chapter 4 . For this project, you need to create only the pages
folder.
Open the home.dart
file, and add to the body
a SafeArea
with TabBarView
as a child
. The TabBarView
controller
property is a TabController
variable called _tabController
. Add to the TabBarView
children
property the Birthdays()
, Gratitude()
, and Reminders()
pages that you'll create in step 3.body: SafeArea(
child: TabBarView(
controller: _tabController,
children: [
Birthdays(),
Gratitude(),
Reminders(),
],
),
),
This time you'll create the pages that you are navigating to first. Like in the BottomNavigationBar
app, create three StatelessWidget
pages and call them Birthdays
, Gratitude
, and Reminders
. Each page has a Scaffold
with Center()
for the body
. The Center
child
is an Icon
with the size
of 120.0 pixels and a color
.The following are the three Dart files, birthdays.dart
, gratitude.dart
, and reminders.dart
:
// birthdays.dart
import 'package:flutter/material.dart';
class Birthdays extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.cake,
size: 120.0,
color: Colors.orange,
),
),
);
}
}
// gratitude.dart
import 'package:flutter/material.dart';
class Gratitude extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.sentiment_satisfied,
size: 120.0,
color: Colors.lightGreen,
),
),
);
}
}
// reminders.dart
import 'package:flutter/material.dart';
class Reminders extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.access_alarm,
size: 120.0,
color: Colors.purple,
),
),
);
}
}
Import each page in the home.dart
file.import 'package:flutter/material.dart';
import 'birthdays.dart';
import 'gratitude.dart';
import 'reminders.dart';
Declare the TickerProviderStateMixin
to the _HomeState
class by adding with
TickerProviderStateMixin
. The AnimationController
vsync
argument will use it.class _HomeState extends State<Home> with SingleTickerProviderStateMixin {…}
Declare a TabController
variable by the name of _tabController
. Override the initState()
method to initialize the _tabController
with the vsync
argument and a length
value of 3. The vsync
is referenced by this
, meaning this reference of the _HomeState
class. The length
represents the number of Tab
s to show. Add a Listener
to the _tabController
to catch when a Tab
is changed. Then override the dispose()
method for when the page closes to properly dispose of the _tabController
.Note in the _tabChanged
method you check for indexIsChanging
before showing which Tab
is tapped. If you do not check for indexIsChanging
, then the code runs twice.
class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 3);
_tabController.addListener(_tabChanged);
}
@override
void dispose() {
super.dispose();
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
_tabController.dispose();
}
void _tabChanged() {
// Check if Tab Controller index is changing, otherwise we get the notice twice
if (_tabController.indexIsChanging) {
print('tabChanged: ${_tabController.index}');
}
}
Add a TabBar
as a child
of the bottomNavigationBar
Scaffold
property.bottomNavigationBar: SafeArea(
child: TabBar(),
),
Pass the _tabController
for the TabBar
controller
property. I customized the labelColor
and unselectedLabelColor
by using, respectively, Colors.black54
and Colors.black38
, but feel free to experiment using different colors.bottomNavigationBar: SafeArea(
child: TabBar(
controller: _tabController,
labelColor: Colors.black54,
unselectedLabelColor: Colors.black38,
),
),
Add three Tab
widgets to the tabs
widget list. Customize each Tab
icon
and text
.bottomNavigationBar: SafeArea(
child: TabBar(
controller: _tabController,
labelColor: Colors.black54,
unselectedLabelColor: Colors.black38,
tabs: [
Tab(
icon: Icon(Icons.cake),
text: 'Birthdays',
),
Tab(
icon: Icon(Icons.sentiment_satisfied),
text: 'Gratitude',
),
Tab(
icon: Icon(Icons.access_alarm),
text: 'Reminders',
),
],
),
),
How It Works
When TabBar
and TabBarView
are used together, the correct associated page is automatically loaded. When the user swipes the TabBarView
left or right, it scrolls to the correct page and selects the corresponding tab in the TabBar
. All of these powerful features are built in; no custom coding is necessary.
How does it know which page belongs to which tab? The TabController
is responsible for syncing tab selections between a TabBar
and a TabBarView
. Since the TabController
syncs the tab selections, you need to declare the SingleTickerProviderStateMixin
to the class.
Both the TabBar
and TabBarView
use the same TabController
. The TabController
is initiated by passing a vsync
argument and a length
argument. The length
argument is the number of tabs to show. An optional TabController
Listener
is added to listen to Tab
changes and take appropriate action if necessary, perhaps saving data before a Tab
is switched. Each Tab
is added to the TabBar
tabs
widget list by customizing each icon
and text
.
TabBarView
is responsible for loading the appropriate page when Tab
selection changes. The page views are listed as the children
property of the TabBarView
widget.
USING THE DRAWER AND LISTVIEW
You might be wondering why I'm covering the ListView
in this navigation chapter. Well, it works great with the Drawer
widget. ListView
widgets are used quite often for selecting an item from a list to navigate to a detailed page.
Drawer
is a Material Design panel that slides horizontally from the left or right edge of the Scaffold
, the device screen. Drawer
is used with the Scaffold
drawer
(left‐side) property or endDrawer
(right‐side) property. Drawer
can be customized for each individual need but usually has a header to show an image or fixed information and a ListView
to show a list of navigable pages. Usually, a Drawer
is used when the navigation list has many items.
To set the Drawer
header, you have two built‐in options, the UserAccountsDrawerHeader
or the DrawerHeader
. The UserAccountsDrawerHeader
is intended to display the app's user details by setting the currentAccountPicture
, accountName
, accountEmail
, otherAccountsPictures
, and decoration
properties.
// User details
UserAccountsDrawerHeader(
currentAccountPicture: Icon(Icons.face,),
accountName: Text('Sandy Smith'),
accountEmail: Text('[email protected] '),
otherAccountsPictures: <Widget>[
Icon(Icons.bookmark_border),
],
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/home_top_mountain.jpg'),
fit: BoxFit.cover,
),
),
),
DrawerHeader
is intended to display generic or custom information by setting the padding
, child
, decoration
, and other properties.
// Generic or custom information
DrawerHeader(
padding: EdgeInsets.zero,
child: Icon(Icons.face),
decoration: BoxDecoration(color: Colors.blue),
),
The standard ListView
constructor allows you to build a short list of items quickly. The next chapter will go into more depth on how to use the ListView
. See Figure 8.4 .
FIGURE 8.4 : Drawer and ListView
TRY IT OUT Creating the Drawer App
In this example, the Drawer
is added to the drawer
or endDrawer
property of the Scaffold
. The drawer
and endDrawer
properties slide the Drawer
from either left to right (TextDirection.ltr
) or right to left (TextDirection.rtl
). In this example, you'll add both the drawer
and the endDrawer
to show how to use both. You use the UserAccountsDrawerHeader
to the drawer
(left‐side) property and the DrawerHeader
to the endDrawer
(right‐side) property.
You use the ListView
to add the Drawer
content and ListTile
to align text and icons for the menu list easily. In this project, you use the standard ListView
constructor since you have a small list of menu items.
Create a new Flutter project and name it ch8_drawer
, following the instructions in Chapter 4 . For this project, you need to create the pages
, widgets
, and assets/images
folders. Copy the home_top_mountain.jpg
image to the assets/images
folder.
Open the pubspec.yaml
file and under assets
add the images
folder.# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
Add the folder assets
and subfolder images
at the project's root and then copy the home_top_mountain.jpg
file to the images
folder.
Click the Save button; depending on the editor you are using, it automatically runs the flutter
packages
get
. Once finished, it shows a message of Process
finished
with
exit
code
0
.If it does not automatically run the command for you, open the Terminal window (located at the bottom of your editor) and type flutter
packages
get
.
Create the pages that you are navigating to first. Create three StatelessWidget
pages and call them Birthdays
, Gratitude
, and Reminders
. Each page has a Scaffold
with Center()
for the body
. The Center
child
is an Icon
with the size
value of 120.0 pixels and a color
property.class Birthdays extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.cake,
size: 120.0,
color: Colors.orange,
),
),
);
}
}
Add the AppBar
to the Scaffold
, which is needed for navigating back to the home page. The following shows the three Dart files, birthdays.dart
, gratitude.dart
, and reminders.dart
:// birthdays.dart
import 'package:flutter/material.dart';
class Birthdays extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Birthdays'),
),
body: Center(
child: Icon(
Icons.cake,
size: 120.0,
color: Colors.orange,
),
),
);
}
}
// gratitude.dart
import 'package:flutter/material.dart';
class Gratitude extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Gratitude'),
),
body: Center(
child: Icon(
Icons.sentiment_satisfied,
size: 120.0,
color: Colors.lightGreen,
),
),
);
}
}
// reminders.dart
import 'package:flutter/material.dart';
class Reminders extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Reminders'),
),
body: Center(
child: Icon(
Icons.access_alarm,
size: 120.0,
color: Colors.purple,
),
),
);
}
}
The left and right Drawer
widgets share the same menu list, and you'll write it first. Create a new Dart file in the widgets
folder. Right‐click the widgets
folder and then select New ➪ Dart File, enter menu_list_tile.dart
, and click the OK button to save.
Import the material.dart
, birthdays.dart
, gratitude.dart
, and reminders.dart
classes (pages). Add a new line and then start typing st ; the autocompletion help opens, so select the stful
abbreviation and give it a name of MenuListTileWidget
.import 'package:flutter/material.dart';
import 'package:ch8_drawer/pages/birthdays.dart';
import 'package:ch8_drawer/pages/gratitude.dart';
import 'package:ch8_drawer/pages/reminders.dart';
The Widget
build(BuildContext
context)
returns a Column
. The Column
children
widget list contains multiple ListTile
s representing each menu item. Add a Divider
widget with the color
property set to Colors.grey
before the last ListTile
widget.@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ListTile(),
ListTile(),
ListTile(),
Divider(color: Colors.grey),
ListTile(),
],
);
}
For each ListTile
, set the leading
property as an Icon
and the title
property as a Text
. For the onTap
property, you first call Navigator.pop()
to close the open Drawer
and then call Navigator.push()
to open the selected page.@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ListTile(
leading: Icon(Icons.cake),
title: Text('Birthdays'),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Birthdays(),
),
);
},
),
ListTile(
leading: Icon(Icons.sentiment_satisfied),
title: Text('Gratitude'),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Gratitude(),
),
);
},
),
ListTile(
leading: Icon(Icons.alarm),
title: Text('Reminders'),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Reminders(),
),
);
},
),
Divider(color: Colors.grey),
ListTile(
leading: Icon(Icons.settings),
title: Text('Setting'),
onTap: () {
Navigator.pop(context);
},
),
],
);
}
Create a new Dart file in the widgets
folder. Right‐click the widgets
folder and then select New ➪ Dart File, enter left_drawer.dart
, and click the OK button to save.
Import the material.dart
library and the menu_list_tile.dart
class.import 'package:flutter/material.dart';
import 'package:ch8_drawer/widgets/menu_list_tile.dart';
Add a new line and then start typing st ; the autocompletion help opens. Select the stful
abbreviation and give it a name of LeftDrawerWidget
.class LeftDrawerWidget extends StatelessWidget {
const LeftDrawerWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer();
}
}
The Widget
build(BuildContext
context)
returns a Drawer
. The Drawer
child
is a ListView
children
list widget of a UserAccountsDrawerHeader
widget and a call to the const
MenuListTileWidget()
widget class. To fill the entire Drawer
space, you set the ListView
padding
property to EdgeInsets.zero
.@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
UserAccountsDrawerHeader(),
const MenuListTileWidget(),
],
),
);
}
For the UserAccountsDrawerHeader
, set the currentAccountPicture
, accountName
, accountEmail
, otherAccountsPictures
, and decoration
properties.@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
UserAccountsDrawerHeader(
currentAccountPicture: Icon(
Icons.face,
size: 48.0,
color: Colors.white,
),
accountName: Text('Sandy Smith'),
accountEmail: Text('[email protected] '),
otherAccountsPictures: <Widget>[
Icon(
Icons.bookmark_border,
color: Colors.white,
)
],
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/home_top_mountain.jpg'),
fit: BoxFit.cover,
),
),
),
const MenuListTileWidget(),
],
),
);
}
Create a new Dart file in the widgets
folder. Right‐click the widgets
folder and then select New ➪ Dart File, enter right_drawer.dart
, and click the OK button to save. Import the material.dart
library and the menu_list_tile.dart
class.import 'package:flutter/material.dart';
import 'package:ch8_drawer/widgets/menu_list_tile.dart';
Add a new line and then start typing st ; the autocompletion help opens. Select the stful
abbreviation and give it a name of RightDrawerWidget
.class RightDrawerWidget extends StatelessWidget {
const RightDrawerWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer();
}
}
The Widget
build(BuildContext
context)
returns a Drawer
. The Drawer
child
is a ListView
children
list of a DrawerHeader
widget and a call to the const
MenuListTileWidget()
widget class. To fill the entire Drawer
space, you set the ListView
padding
property to EdgeInsets.zero
.@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(),
const MenuListTileWidget(),
],
),
);
}
For the DrawerHeader
, set the padding
, child
, and decoration
properties.@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
padding: EdgeInsets.zero,
child: Icon(
Icons.face,
size: 128.0,
color: Colors.white54,
),
decoration: BoxDecoration(color: Colors.blue),
),
const MenuListTileWidget(),
],
),
);
}
Open the home.dart
file and import the material.dart
, birthdays.dart
, gratitude.dart
, and reminders.dart
classes.import 'package:flutter/material.dart';
import 'birthdays.dart';
import 'gratitude.dart';
import 'reminders.dart';
Add to the body
a SafeArea
with a Container
as a child
.body: SafeArea(
child: Container(),
),
Add to the Scaffold
drawer
property a call to the LeftDrawerWidget()
widget class and for the endDrawer
property a call to the RightDrawerWidget()
widget class. Note that you use the const
keyword before calling each widget class to take advantage of caching and subtree rebuilding for better performance.return Scaffold(
appBar: AppBar(
title: Text('Drawer'),
),
drawer: const LeftDrawerWidget(),
endDrawer: const RightDrawerWidget(),
body: SafeArea(
child: Container(),
),
);
How It Works
To add a Drawer
to an app, you set the Scaffold
drawer
or endDrawer
property. The drawer
and endDrawer
properties slide the Drawer
from either left to right (TextDirection.ltr
) or right to left (TextDirection.rtl
).
The Drawer
widget takes a child
property, and you passed a ListView
. Using a ListView
allows you to create a scrollable menu list of items. For the ListView
children
widget list, you created two widget classes, one to build the Drawer
header and one to build the list of menu items. To set the Drawer
header, you have two options, UserAccountsDrawerHeader
or DrawerHeader
. These two widgets easily allow you to set header content depending on the requirements. You looked at two examples for the Drawer
header by calling the appropriate widget class LeftDrawerWidget()
or RightDrawerWidget()
.
For the menu items list, you used the MenuListTileWidget()
widget class. This class returns a Column
widget that uses the ListTile
to build your menu list. The ListTile
widget allows you to set the leading
Icon
, title
, and onTap
properties. The onTap
property calls Navigator.pop()
to close Drawer
and calls Navigator.push()
to navigate to the selected page.
This app is an excellent example of creating a shallow widget tree by using widget classes and separating them into individual files for maximum reuse. You also nested widget classes with the left and right widget classes both calling the menu list class.
SUMMARY
In this chapter, you learned to use the Navigator
widget to manage a stack of routes so as to allow navigation between pages. You optionally passed data to the navigation page and back to the original page. The hero animation allows a widget transition to fly into place from one page to another. The widget to animate from and to is wrapped in a Hero
widget by a unique key.
You used the BottomNavigationBar
widget to display a horizontal list of BottomNavigationBarItem
s containing an icon
and a title
at the bottom of the page. When the user taps each BottomNavigationBarItem
, the appropriate page is displayed. To enhance the look of a bottom navigation bar, you used the BottomAppBar
widget and enabled the optional notch. The notch is the result of embedding a FloatingActionButton
to a BottomAppBar
by setting the BottomAppBar
shape
to a CircularNotchedRectangle()
class and setting the Scaffold
floatingActionButtonLocation
property to FloatingActionButtonLocation.endDocked
.
The TabBar
widget displays a horizontal row of tabs. The tabs
property takes a List
of widgets, and tabs are added by using the Tab
widget. The TabBarView
widget is used in conjunction with the TabBar
widget to display the page of the selected tab. Users can swipe left or right to change content or tap each Tab
. The TabController
class handled the syncing of the TabBar
and selected TabBarView
. The TabController
requires the use of with
SingleTickerProviderStateMixin
in the class.
The Drawer
widget allows the user to slide a panel from left or right. The Drawer
widget is added by setting the Scaffold
drawer
or endDrawer
property. To easily align menu items in a list, you pass a ListView
as a child
of the Drawer
. Since this menu list is short, you use the standard ListView
constructor instead of a ListView
builder, which is covered in the next chapter. You have two prebuilt drawer header options, UserAccountsDrawerHeader
or DrawerHeader
. When the user taps one of the menu items, the onTap
property calls Navigator.pop()
to close Drawer
and calls Navigator.push()
to navigate to the selected page.
In the next chapter, you'll learn to use different types of lists. You will take a look at the ListView
, GridView
, Stack
, Card
, and my favorite—CustomScrollView
to use Sliver
s.
WHAT YOU LEARNED IN THIS CHAPTER
TOPIC
KEY CONCEPTS
Navigator
The Navigator
widget manages a stack of routes to move between pages.
Hero animation
The Hero
widget is used to convey a navigation animation and size for a widget to fly into place from one page to another. When the second page is dismissed, the animation is reversed to the originating page.
BottomNavigationBar
BottomNavigationBar
displays a horizontal list of BottomNavigationBarItem
items containing an icon and a title at the bottom of the page.
BottomAppBar
The BottomAppBar
widget behaves like the BottomNavigationBar
, but it has an optional notch along the top.
TabBar
The TabBar
widget displays a horizontal row of tabs. The tabs
property takes a list of widgets, and tabs are added by using the Tab
widget.
TabBarView
The TabBarView
widget is used in conjunction with the TabBar
widget to display the page of the selected tab.
Drawer
The Drawer
widget allows the user to slide a panel from left or right.
ListView
The standard ListView
constructor allows you to build a short list of items quickly.